-
Notifications
You must be signed in to change notification settings - Fork 0
Home
- Introduction
- The Need for Data Aggregation
- Core Concepts
- Package Overview
- Getting Started
- Architecture Deep Dive
- Configuration Guide
- Query Implementation Guide
- Transformer Implementation Guide
- Advanced Features
- Extension Points
- Best Practices
- Performance Considerations
- Troubleshooting
- Future Extensions
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.
- 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
In today's microservices and distributed system architectures, applications often need to:
-
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
-
Handle Different Data Formats
- SQL database records
- JSON responses from REST APIs
- XML from legacy systems
- NoSQL document stores
-
Manage Complex Dependencies
- Parent-child relationships across systems
- Conditional data loading based on context
- Performance optimization through selective loading
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 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
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 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 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;
}
}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 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
Purpose: Foundation package providing core interfaces and implementations.
Key Components:
-
IEntity,IQuery,ITransformerinterfaces -
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.CorePurpose: 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.SQLSupported Databases:
- SQL Server
- SQLite
- MySQL
- PostgreSQL
- Oracle (with appropriate providers)
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.EntityFrameworkPurpose: 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 | .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+ |
First, install the required packages:
Install-Package Schemio.Core
Install-Package Schemio.SQL # For SQL database support
Install-Package Schemio.API # For REST API supportpublic 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; }
}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; }
}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);
}
}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;
}
}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();
}
}// 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);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);
}
}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]
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;
}The QueryBuilder<T> creates an optimized execution plan:
- Filter by Schema Paths: Only include queries matching requested paths
- Resolve Dependencies: Build parent-child query relationships
- Optimize Execution: Determine optimal query execution order
The QueryExecutor manages parallel execution with dependency resolution:
- Level-by-Level Execution: Execute queries level by level to handle dependencies
- Parallel Processing: Run independent queries at the same level in parallel
- Result Propagation: Pass parent results to child queries
-
Caching: Cache results marked with
[CacheResult]attribute
The EntityBuilder<T> assembles the final entity:
- Transformer Resolution: Match query results to appropriate transformers
- Sequential Application: Apply transformers in dependency order
- Type Safety: Ensure type compatibility between results and transformers
- Query Results: Results are collected and passed to transformers
- Caching: Optional caching for expensive operations
- Disposal: Proper disposal of database connections and HTTP clients
return CreateSchema.For<Customer>()
.Map<CustomerQuery, CustomerTransformer>(For.Paths("customer"))
.Map<AddressQuery, AddressTransformer>(For.Paths("customer/address"))
.End();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();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();-
customer- Exact match -
customer/orders- Nested path -
customer/orders/order/items- Deep nesting -
//orders- Descendant matching (with ancestor support)
-
$.customer- Root level -
$.customer.orders- Nested property -
$.customer.orders[*].items- Array elements
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
};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 });
}
}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);
};
}
}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);
};
}
}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;
};
}
}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);
};
}
}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);
}
}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";
}
}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);
}
}public class CustomerResult : IQueryResult
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string Email { get; set; }
}public class CollectionResult<T> : List<T>, IQueryResult
{
public CollectionResult(IEnumerable<T> items) : base(items) { }
public CollectionResult() { }
}public class UserApiResult : WebHeaderResult
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
// Headers property inherited from WebHeaderResult
}[CacheResult]
public class CategoryResult : IQueryResult
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}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;
}
}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();
}
}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;
}
}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;
}
}
}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);
}
}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>();
}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("<", "<").Replace(">", ">");
}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}";
}
}Schemio provides built-in caching for expensive query results:
[CacheResult]
public class CategoryResult : IQueryResult
{
public int Id { get; set; }
public string Name { get; set; }
}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;
}
}
}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" }
});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
}
}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];
}
}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();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());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);
}
}public interface IRedisQuery : IQuery
{
Task<IQueryResult> Execute(IDatabase database);
}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);
}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);
};
}
}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];
}
}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}");
}
}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; }
}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;
}// 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
}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; }
}// 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
}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);
};
}
}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";
}
}
}// 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();
}
}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();
}
}// 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>(...);
};
}
}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);
};
}
}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" }
};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();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";
}
}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
}
}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;
};
}
}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;
}
};
}
}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();
}
}
}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;
}
}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);
}
}
}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
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
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
services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Debug);
});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;
}
}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");
}
}
}TBC