Skip to content
Najaf Shaikh edited this page Aug 15, 2025 · 22 revisions

Schemio Framework - Complete Documentation

Table of Contents

  1. Introduction
  2. The Need for Data Aggregation
  3. Core Concepts
  4. Package Overview
  5. Getting Started
  6. Architecture Deep Dive
  7. Configuration Guide
  8. Query Implementation Guide
  9. Transformer Implementation Guide
  10. Advanced Features
  11. Extension Points
  12. Best Practices
  13. Performance Considerations
  14. Troubleshooting
  15. Future Extensions

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

    • User profiles from identity services
    • Order history from e-commerce APIs
    • Product catalogs from different databases
    • Analytics data from various platforms
  2. Handle Different Data Formats

    • SQL database records
    • JSON responses from REST APIs
    • XML from legacy systems
    • NoSQL document stores
  3. Manage Complex Dependencies

    • Parent-child relationships across systems
    • Conditional data loading based on context
    • Performance optimization through selective loading

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:

public class Customer : IEntity
{
    public int CustomerId { get; set; }
    public string CustomerCode { get; set; }
    public string CustomerName { get; set; }
    public Communication Communication { get; set; }
    public Order[] Orders { get; set; }
}

Queries

Queries fetch data from specific data sources. Each query extends BaseQuery<TResult>:

public class CustomerQuery : SQLQuery<CustomerResult>
{
    protected override Func<IDbConnection, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var customer = (CustomerRequest)context.Request;
        return connection => connection.QueryFirstOrDefaultAsync<CustomerResult>(...);
    }
}

Transformers

Transformers map query results to entity properties:

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

Entity Configuration

Configuration defines the schema mappings between paths and query/transformer pairs:

public class CustomerConfiguration : EntityConfiguration<Customer>
{
    public override IEnumerable<Mapping<Customer, IQueryResult>> GetSchema()
    {
        return CreateSchema.For<Customer>()
            .Map<CustomerQuery, CustomerTransform>(For.Paths("customer"))
            .End();
    }
}

Schema Paths

Schema paths define the object graph structure using XPath or JSONPath:

  • customer - Root level
  • customer/communication - Nested communication data
  • customer/orders - Collection of orders
  • customer/orders/order/items - Deep nesting

Package Overview

Core Packages

1. Schemio.Core

Purpose: Foundation package providing core interfaces and implementations.

Key Components:

  • IEntity, IQuery, ITransformer interfaces
  • DataProvider<T> - Main orchestration class
  • QueryBuilder<T> - Builds query execution plan
  • EntityBuilder<T> - Assembles final entity
  • Path matchers for XPath and JSONPath

Installation:

Install-Package Schemio.Core

2. Schemio.SQL

Purpose: SQL database support using Dapper for query execution.

Key Components:

  • SQLQuery<TResult> - Base class for SQL queries
  • QueryEngine - Dapper-based query execution
  • SQLConfiguration - Connection and query settings

Installation:

Install-Package Schemio.SQL

Supported Databases:

  • SQL Server
  • SQLite
  • MySQL
  • PostgreSQL
  • Oracle (with appropriate providers)

3. Schemio.EntityFramework

Purpose: Entity Framework Core integration for advanced ORM scenarios.

Key Components:

  • SQLQuery<TResult> - EF Core query implementation
  • QueryEngine<T> - DbContext factory integration
  • Full LINQ query support

Installation:

Install-Package Schemio.EntityFramework

4. Schemio.API

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

Key Components:

  • WebQuery<TResult> - Base class for API queries
  • QueryEngine - HttpClient-based execution
  • WebHeaderResult - Support for response headers
  • Request/response header management

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

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

graph TD
    A[Client Request] --> B[DataProvider]
    B --> C[QueryBuilder]
    C --> D[Generate Query Plan]
    D --> E[QueryExecutor]
    E --> F[Execute Level 1 Queries]
    F --> G[Resolve Dependencies]
    G --> H[Execute Level 2 Queries]
    H --> I[Continue Until All Levels Complete]
    I --> J[EntityBuilder]
    J --> K[Apply Transformers]
    K --> L[Return Aggregated Entity]
Loading

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

  • Query Results: Results are collected and passed to transformers
  • Caching: Optional caching for expensive operations
  • Disposal: Proper disposal of database connections and HTTP clients

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

  • customer - Exact match
  • customer/orders - Nested path
  • customer/orders/order/items - Deep nesting
  • //orders - Descendant matching (with ancestor support)

JSONPath Patterns

  • $.customer - Root level
  • $.customer.orders - Nested property
  • $.customer.orders[*].items - Array elements

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

Complex EF Query with Includes

public class OrdersQuery : SQLQuery<CollectionResult<OrderResult>>
{
    protected override Func<DbContext, Task<CollectionResult<OrderResult>>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var customer = (CustomerResult)parentQueryResult;
        
        return async dbContext =>
        {
            var orders = await dbContext.Set<OrderEntity>()
                .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
                .Where(o => o.CustomerId == customer.Id)
                .Select(o => new OrderResult
                {
                    OrderId = o.OrderId,
                    OrderNumber = o.OrderNumber,
                    OrderDate = o.OrderDate,
                    TotalAmount = o.TotalAmount,
                    ItemCount = o.OrderItems.Count
                })
                .ToListAsync();
                
            return new CollectionResult<OrderResult>(orders);
        };
    }
}

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

Dependent API Query

public class UserPostsQuery : WebQuery<CollectionResult<PostResult>>
{
    public UserPostsQuery() : base("https://api.blog.com/") { }

    protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
    {
        var user = (UserProfileResult)parentQueryResult;
        return () => new Uri($"users/{user.Id}/posts?limit=10", UriKind.Relative);
    }
}

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() { }
}

Web Result with Headers

public class UserApiResult : WebHeaderResult
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    // Headers property inherited from WebHeaderResult
}

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
        // This could use cached data or make additional queries
        return false;
    }

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

Conditional Transformation

public class OrderTransformer : BaseTransformer<OrderResult, Customer>
{
    public override void Transform(OrderResult queryResult, Customer entity)
    {
        if (entity.Orders == null)
            entity.Orders = new List<Order>();

        var order = new Order
        {
            OrderId = queryResult.OrderId,
            OrderNumber = queryResult.OrderNumber,
            OrderDate = queryResult.OrderDate,
            TotalAmount = queryResult.TotalAmount
        };

        // Apply business rules
        ApplyOrderStatus(order, queryResult);
        ApplyDiscounts(order, queryResult);

        entity.Orders.Add(order);
    }

    private void ApplyOrderStatus(Order order, OrderResult result)
    {
        order.Status = result.OrderDate > DateTime.Now.AddDays(-30) 
            ? "Recent" 
            : "Historical";
    }

    private void ApplyDiscounts(Order order, OrderResult result)
    {
        if (result.TotalAmount > 100)
        {
            order.HasDiscount = true;
            order.DiscountAmount = result.TotalAmount * 0.1m;
        }
    }
}

Polymorphic Transformation

public class PaymentTransformer : BaseTransformer<PaymentResult, Order>
{
    public override void Transform(PaymentResult queryResult, Order entity)
    {
        switch (queryResult.PaymentType)
        {
            case "CreditCard":
                entity.Payment = new CreditCardPayment
                {
                    CardNumber = MaskCardNumber(queryResult.CardNumber),
                    ExpiryDate = queryResult.ExpiryDate
                };
                break;

            case "PayPal":
                entity.Payment = new PayPalPayment
                {
                    PayPalAccount = queryResult.PayPalEmail
                };
                break;

            case "BankTransfer":
                entity.Payment = new BankTransferPayment
                {
                    AccountNumber = MaskAccountNumber(queryResult.AccountNumber),
                    BankCode = queryResult.BankCode
                };
                break;

            default:
                entity.Payment = new GenericPayment
                {
                    PaymentMethod = queryResult.PaymentType
                };
                break;
        }

        entity.Payment.Amount = queryResult.Amount;
        entity.Payment.TransactionDate = queryResult.TransactionDate;
    }

    private string MaskCardNumber(string cardNumber)
    {
        if (string.IsNullOrEmpty(cardNumber) || cardNumber.Length < 4)
            return cardNumber;

        return "****-****-****-" + cardNumber.Substring(cardNumber.Length - 4);
    }

    private string MaskAccountNumber(string accountNumber)
    {
        if (string.IsNullOrEmpty(accountNumber) || accountNumber.Length < 4)
            return accountNumber;

        return "****" + accountNumber.Substring(accountNumber.Length - 4);
    }
}

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

private string SanitizeHtml(string input)
{
    if (string.IsNullOrEmpty(input))
        return string.Empty;
        
    // Basic HTML sanitization - use a proper library in production
    return input.Replace("<", "&lt;").Replace(">", "&gt;");
}

3. Performance Optimization

public class OptimizedTransformer : BaseTransformer<CollectionResult<ItemResult>, Customer>
{
    private static readonly ConcurrentDictionary<int, string> CategoryCache 
        = new ConcurrentDictionary<int, string>();

    public override void Transform(CollectionResult<ItemResult> queryResult, Customer entity)
    {
        if (queryResult == null || !queryResult.Any())
            return;

        // Use parallel processing for large collections
        var items = queryResult.AsParallel()
            .Select(CreateItem)
            .ToArray();

        entity.Items = items;
    }

    private Item CreateItem(ItemResult result)
    {
        return new Item
        {
            ItemId = result.Id,
            Name = result.Name,
            CategoryName = GetCategoryName(result.CategoryId)
        };
    }

    private string GetCategoryName(int categoryId)
    {
        return CategoryCache.GetOrAdd(categoryId, id => 
        {
            // Expensive lookup - only done once per category
            return LookupCategoryName(id);
        });
    }

    private string LookupCategoryName(int categoryId)
    {
        // Implementation would lookup category name
        return $"Category {categoryId}";
    }
}

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

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

Transformer-Level Error Handling

public class SafeTransformer : BaseTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult queryResult, Customer entity)
    {
        try
        {
            entity.CustomerId = queryResult.Id;
            entity.CustomerName = queryResult.Name;
            // ... other transformations
        }
        catch (Exception ex)
        {
            // Log error and apply default values
            Logger.LogError(ex, "Error transforming customer data");
            ApplyDefaultValues(entity);
        }
    }

    private void ApplyDefaultValues(Customer entity)
    {
        entity.CustomerName ??= "Unknown Customer";
        entity.Orders ??= new Order[0];
    }
}

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

Custom Schema Path Matching

Implement custom path matching logic:

public class CustomPathMatcher : ISchemaPathMatcher
{
    public bool IsMatch(string inputPath, ISchemaPaths configuredPaths)
    {
        // Custom matching logic
        return configuredPaths.Paths.Any(configuredPath => 
            CustomMatch(inputPath, configuredPath));
    }

    private bool CustomMatch(string inputPath, string configuredPath)
    {
        // Implement your custom matching algorithm
        // For example: regex matching, wildcard patterns, etc.
        return inputPath.EndsWith(configuredPath, StringComparison.OrdinalIgnoreCase);
    }
}

// Register custom matcher
services.UseSchemio()
    .WithPathMatcher(c => new CustomPathMatcher());

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

Example Redis Query

public class UserCacheQuery : RedisQuery<UserCacheResult>
{
    protected override Func<IDatabase, Task<UserCacheResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (UserRequest)context.Request;
        
        return async database =>
        {
            var key = $"user:{request.UserId}";
            var cachedData = await database.StringGetAsync(key);
            
            if (!cachedData.HasValue)
                return null;

            return JsonSerializer.Deserialize<UserCacheResult>(cachedData);
        };
    }
}

Creating Custom Transformers

Generic Collection Transformer

public abstract class CollectionTransformer<TResult, TEntity, TItem> 
    : BaseTransformer<CollectionResult<TResult>, TEntity>
    where TEntity : IEntity
    where TResult : IQueryResult
{
    public override void Transform(CollectionResult<TResult> queryResult, TEntity entity)
    {
        if (queryResult == null || !queryResult.Any())
        {
            SetEmptyCollection(entity);
            return;
        }

        var items = queryResult.Select(TransformItem).ToArray();
        SetCollection(entity, items);
    }

    protected abstract TItem TransformItem(TResult result);
    protected abstract void SetCollection(TEntity entity, TItem[] items);
    protected abstract void SetEmptyCollection(TEntity entity);
}

// Usage
public class OrdersTransformer : CollectionTransformer<OrderResult, Customer, Order>
{
    protected override Order TransformItem(OrderResult result)
    {
        return new Order
        {
            OrderId = result.OrderId,
            OrderNumber = result.OrderNumber,
            OrderDate = result.OrderDate
        };
    }

    protected override void SetCollection(Customer entity, Order[] items)
    {
        entity.Orders = items;
    }

    protected override void SetEmptyCollection(Customer entity)
    {
        entity.Orders = new Order[0];
    }
}

Validation Transformer

public abstract class ValidatingTransformer<TResult, TEntity> 
    : BaseTransformer<TResult, TEntity>
    where TEntity : IEntity
    where TResult : IQueryResult
{
    public override void Transform(TResult queryResult, TEntity entity)
    {
        var validationErrors = ValidateResult(queryResult);
        
        if (validationErrors.Any())
        {
            HandleValidationErrors(validationErrors, queryResult, entity);
            return;
        }

        PerformTransform(queryResult, entity);
    }

    protected abstract List<string> ValidateResult(TResult result);
    protected abstract void PerformTransform(TResult result, TEntity entity);
    
    protected virtual void HandleValidationErrors(
        List<string> errors, TResult result, TEntity entity)
    {
        var errorMessage = string.Join(", ", errors);
        throw new TransformationException($"Validation failed: {errorMessage}");
    }
}

Custom Result Types

Timestamped Result

public abstract class TimestampedResult : IQueryResult
{
    public DateTime RetrievedAt { get; set; } = DateTime.UtcNow;
    public string Source { get; set; }
}

public class TimestampedCustomerResult : TimestampedResult
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Paginated Result

public class PaginatedResult<T> : IQueryResult
{
    public List<T> Items { get; set; } = new List<T>();
    public int TotalCount { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public bool HasNextPage => PageNumber * PageSize < TotalCount;
    public bool HasPreviousPage => PageNumber > 1;
}

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
public class CustomerGod : IEntity
{
    // Too many responsibilities
    public int CustomerId { get; set; }
    public string Name { get; set; }
    // ... 50+ properties
    public PaymentHistory[] PaymentHistories { get; set; }
    public SupportTicket[] SupportTickets { get; set; }
    public MarketingPreference[] MarketingPreferences { get; set; }
    // ... many more
}

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 query
public class CustomerBasicInfoQuery : SQLQuery<CustomerResult>
{
    // Fetches only basic customer information
}

// Good: Separate query for orders
public class CustomerOrdersQuery : SQLQuery<CollectionResult<OrderResult>>
{
    // Fetches only customer orders
}

// Avoid: Monolithic query
public class CustomerEverythingQuery : SQLQuery<CustomerMegaResult>
{
    // Tries to fetch everything in one query - hard to maintain
}

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

3. Transformation Best Practices

Defensive Programming

public class SafeCustomerTransformer : BaseTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult queryResult, Customer entity)
    {
        if (queryResult == null)
        {
            ApplyDefaults(entity);
            return;
        }

        entity.CustomerId = queryResult.Id;
        entity.Name = SanitizeString(queryResult.Name) ?? "Unknown";
        entity.Email = ValidateEmail(queryResult.Email);
        entity.CreatedDate = queryResult.CreatedDate ?? DateTime.MinValue;
    }

    private void ApplyDefaults(Customer entity)
    {
        entity.Name = "Unknown Customer";
        entity.Email = "no-email@example.com";
        entity.CreatedDate = DateTime.MinValue;
    }

    private string SanitizeString(string input)
    {
        return string.IsNullOrWhiteSpace(input) ? null : input.Trim();
    }

    private string ValidateEmail(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return "no-email@example.com";
            
        try
        {
            var addr = new System.Net.Mail.MailAddress(email);
            return addr.Address;
        }
        catch
        {
            return "invalid-email@example.com";
        }
    }
}

4. Configuration Organization

Modular Configurations

// Base configuration with common patterns
public abstract class BaseEntityConfiguration<T> : EntityConfiguration<T> 
    where T : IEntity
{
    protected IMappings<T, IQueryResult> CreateBaseSchema()
    {
        return CreateSchema.For<T>();
    }
    
    protected ISchemaPaths AuditPaths(string basePath)
    {
        return For.Paths($"{basePath}/audit", $"{basePath}/metadata");
    }
}

// Specific configurations extend base
public class CustomerConfiguration : BaseEntityConfiguration<Customer>
{
    public override IEnumerable<Mapping<Customer, IQueryResult>> GetSchema()
    {
        return CreateBaseSchema()
            .Map<CustomerQuery, CustomerTransformer>(For.Paths("customer"))
            .Map<AuditQuery, AuditTransformer>(AuditPaths("customer"))
            .End();
    }
}

5. Error Handling Strategy

Graceful Degradation

public class ResilientDataProvider<T> : IDataProvider<T> where T : IEntity, new()
{
    private readonly IDataProvider<T> innerProvider;
    private readonly ILogger<ResilientDataProvider<T>> logger;

    public T GetData(IEntityRequest request)
    {
        try
        {
            return innerProvider.GetData(request);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get complete data, returning partial result");
            return GetPartialData(request);
        }
    }

    private T GetPartialData(IEntityRequest request)
    {
        // Try to get minimal data or return default
        var partialRequest = new PartialEntityRequest
        {
            SchemaPaths = new[] { GetRootPath() }
        };
        
        try
        {
            return innerProvider.GetData(partialRequest);
        }
        catch
        {
            return new T(); // Return empty entity as last resort
        }
    }

    private string GetRootPath()
    {
        return typeof(T).Name.ToLower();
    }
}

6. Performance Optimization

Connection Management

// Good: Let Schemio manage connections
public class EfficientQuery : SQLQuery<CustomerResult>
{
    protected override Func<IDbConnection, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        // Connection is managed by the framework
        return async connection =>
        {
            // Use the provided connection
            return await connection.QueryFirstOrDefaultAsync<CustomerResult>(...);
        };
    }
}

Batch Operations

public class BatchOrdersQuery : SQLQuery<CollectionResult<OrderResult>>
{
    protected override Func<IDbConnection, Task<CollectionResult<OrderResult>>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var customers = (CollectionResult<CustomerResult>)parentQueryResult;
        
        return async connection =>
        {
            if (!customers.Any()) 
                return new CollectionResult<OrderResult>();

            // Batch query for all customers
            var customerIds = customers.Select(c => c.Id).ToList();
            var orders = await connection.QueryAsync<OrderResult>(
                "SELECT * FROM Orders WHERE CustomerId IN @CustomerIds",
                new { CustomerIds = customerIds });
                
            return new CollectionResult<OrderResult>(orders);
        };
    }
}

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

// Access cached data in transformers
public class AddressTransformer : BaseTransformer<AddressResult, Customer>
{
    public override void Transform(AddressResult result, Customer entity)
    {
        entity.Address = new Address
        {
            Street = result.Street,
            City = result.City,
            CountryName = GetCountryName(result.CountryId)
        };
    }

    private string GetCountryName(int countryId)
    {
        if (Context.Cache.TryGetValue("CountryResult", out var cached))
        {
            var countries = (CollectionResult<CountryResult>)cached;
            return countries.FirstOrDefault(c => c.Id == countryId)?.Name ?? "Unknown";
        }
        return "Unknown";
    }
}

Memory Management

1. Stream Processing for Large Collections

public class StreamingOrdersQuery : SQLQuery<CollectionResult<OrderResult>>
{
    protected override Func<IDbConnection, Task<CollectionResult<OrderResult>>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        return async connection =>
        {
            // Use streaming for large result sets
            var orders = new List<OrderResult>();
            
            using var reader = await connection.ExecuteReaderAsync(
                "SELECT * FROM Orders WHERE CustomerId = @CustomerId ORDER BY OrderDate",
                new { CustomerId = ((CustomerResult)parentQueryResult).Id });
                
            while (await reader.ReadAsync())
            {
                orders.Add(new OrderResult
                {
                    OrderId = reader.GetInt32("OrderId"),
                    OrderDate = reader.GetDateTime("OrderDate"),
                    // ... other fields
                });
                
                // Process in batches to control memory usage
                if (orders.Count >= 1000)
                {
                    // Process batch and clear
                    ProcessBatch(orders);
                    orders.Clear();
                }
            }
            
            return new CollectionResult<OrderResult>(orders);
        };
    }
    
    private void ProcessBatch(List<OrderResult> orders)
    {
        // Process batch if needed
    }
}

2. Disposal Patterns

public class ResourceAwareQuery : SQLQuery<CustomerResult>
{
    protected override Func<IDbConnection, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        return async connection =>
        {
            using var command = connection.CreateCommand();
            command.CommandText = "SELECT * FROM Customers WHERE Id = @Id";
            command.Parameters.Add(new SqlParameter("@Id", context.Request.CustomerId));
            
            using var reader = await command.ExecuteReaderAsync();
            if (await reader.ReadAsync())
            {
                return new CustomerResult
                {
                    Id = reader.GetInt32("Id"),
                    Name = reader.GetString("Name")
                };
            }
            
            return null;
        };
    }
}

Monitoring and Diagnostics

1. Custom Logging

public class LoggingQuery : SQLQuery<CustomerResult>
{
    private readonly ILogger<LoggingQuery> logger;

    public LoggingQuery(ILogger<LoggingQuery> logger)
    {
        this.logger = logger;
    }

    protected override Func<IDbConnection, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        return async connection =>
        {
            var stopwatch = Stopwatch.StartNew();
            
            try
            {
                logger.LogInformation("Executing customer query for ID: {CustomerId}", 
                    context.Request.CustomerId);
                    
                var result = await connection.QueryFirstOrDefaultAsync<CustomerResult>(...);
                
                stopwatch.Stop();
                logger.LogInformation("Customer query completed in {ElapsedMs}ms", 
                    stopwatch.ElapsedMilliseconds);
                    
                return result;
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                logger.LogError(ex, "Customer query failed after {ElapsedMs}ms", 
                    stopwatch.ElapsedMilliseconds);
                throw;
            }
        };
    }
}

2. Performance Metrics

public class MetricsCollectingDataProvider<T> : IDataProvider<T> where T : IEntity
{
    private readonly IDataProvider<T> innerProvider;
    private readonly IMetrics metrics;

    public T GetData(IEntityRequest request)
    {
        var timer = metrics.StartTimer($"data_provider_{typeof(T).Name}");
        
        try
        {
            var result = innerProvider.GetData(request);
            metrics.IncrementCounter($"data_provider_{typeof(T).Name}_success");
            return result;
        }
        catch (Exception ex)
        {
            metrics.IncrementCounter($"data_provider_{typeof(T).Name}_error", 
                new[] { ("error_type", ex.GetType().Name) });
            throw;
        }
        finally
        {
            timer.Dispose();
        }
    }
}

Troubleshooting

Common Issues and Solutions

1. Query Not Executing

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

Causes & Solutions:

  • Path mismatch: Ensure schema paths in configuration match request paths
  • Query engine mismatch: Verify the correct query engine is registered
  • Context resolution failure: Check IsContextResolved() returns true
// Debug query resolution
public class DebuggingQuery : SQLQuery<CustomerResult>
{
    protected override Func<IDbConnection, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        System.Diagnostics.Debug.WriteLine($"Query resolved for context: {context.Request.GetType()}");
        return connection => /* ... */;
    }

    public override bool IsContextResolved()
    {
        var resolved = base.IsContextResolved();
        System.Diagnostics.Debug.WriteLine($"Query context resolved: {resolved}");
        return resolved;
    }
}

2. Transformation Errors

Problem: Data not mapped correctly to entity.

Solutions:

  • Null checks: Always check for null query results
  • Type compatibility: Ensure transformer types match query result types
  • Property mapping: Verify property names and types align
public class SafeTransformer : BaseTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult queryResult, Customer entity)
    {
        if (queryResult == null)
        {
            System.Diagnostics.Debug.WriteLine("Warning: Null query result in transformer");
            return;
        }

        try
        {
            entity.CustomerId = queryResult.Id;
            entity.Name = queryResult.Name ?? "Unknown";
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"Transformation error: {ex.Message}");
            throw new TransformationException($"Failed to transform {typeof(CustomerResult).Name}", ex);
        }
    }
}

3. Dependency Resolution Issues

Problem: Child queries not receiving parent results.

Solutions:

  • Verify nesting structure: Ensure proper parent-child relationships
  • Check result types: Parent query result type must match child query expectation
  • Review execution order: Dependencies must be properly ordered

4. Performance Problems

Problem: Slow query execution or high memory usage.

Solutions:

  • Enable parallel execution: Structure queries to run independently
  • Use selective loading: Only load required data paths
  • Implement caching: Cache expensive or static data
  • Optimize SQL queries: Use proper indexing and query optimization

5. Connection Issues

Problem: Database or API connection failures.

Solutions:

  • Connection string validation: Verify connection strings and credentials
  • Network connectivity: Check network access to data sources
  • Timeout configuration: Adjust timeout settings for slow operations
  • Retry logic: Implement retry mechanisms for transient failures

Debugging Tips

1. Enable Detailed Logging

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

2. Trace Query Execution

public class TracingDataProvider<T> : IDataProvider<T> where T : IEntity
{
    private readonly IDataProvider<T> innerProvider;
    private readonly ILogger logger;

    public T GetData(IEntityRequest request)
    {
        logger.LogDebug("Starting data retrieval for {EntityType} with paths: {Paths}", 
            typeof(T).Name, string.Join(", ", request.SchemaPaths ?? new[] { "all" }));
            
        var result = innerProvider.GetData(request);
        
        logger.LogDebug("Completed data retrieval for {EntityType}", typeof(T).Name);
        
        return result;
    }
}

3. 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)}");
        }
        
        // Check for missing transformers
        var missingTransformers = mappings.Where(m => m.Transformer == null);
        if (missingTransformers.Any())
        {
            throw new InvalidOperationException("Some mappings are missing transformers");
        }
    }
}

Future Extensions

Potential Framework Enhancements

TBC

Clone this wiki locally