Best Practices
Comprehensive guidelines for configuring and extending Scryber effectively.
Configuration Management
JSON Configuration Files
Structure your configuration clearly:
{
"Scryber": {
"Parsing": {
"Namespaces": [ /* Custom component namespaces */ ]
},
"Fonts": {
"Register": [ /* Custom fonts */ ]
},
"Imaging": {
"Factories": [ /* Custom image loaders */ ]
},
"Tracing": {
"TraceLevel": "Messages",
"LogOutput": true
}
}
}
β Do:
- Use relative paths for portability:
"Fonts/Roboto-Regular.ttf" - Externalize connection strings and API keys
- Version configuration files with application code
- Document configuration options inline with comments (when JSON supports it)
- Use environment-specific configuration files
β Donβt:
- Hard-code production credentials in configuration files
- Use absolute paths unless absolutely necessary
- Mix application and Scryber configuration concerns
- Leave sensitive data in source control
Environment-Specific Configuration
var config = new ConfigurationBuilder()
.AddJsonFile("scrybersettings.json", optional: false)
.AddJsonFile($"scrybersettings.{env}.json", optional: true)
.AddEnvironmentVariables()
.Build();
Development (scrybersettings.Development.json):
{
"Scryber": {
"Tracing": {
"TraceLevel": "Verbose",
"LogOutput": true
}
}
}
Production (scrybersettings.Production.json):
{
"Scryber": {
"Tracing": {
"TraceLevel": "Warnings",
"LogOutput": false
}
}
}
Document Controllers
Controller Design
β Do:
- Keep controllers focused on coordination, not business logic
- Use dependency injection for services
- Mark outlets as
Requiredwhen template contract demands them - Use descriptive action method names
- Test controllers independently of PDF generation
- Log significant operations and errors
- Handle missing optional outlets gracefully
β Donβt:
- Put business logic in controllers
- Access database directly (use services)
- Throw exceptions in action methods without handling
- Mutate global state
- Create tightly coupled controllers
Example: Well-Designed Controller
public class InvoiceController
{
private readonly IInvoiceService _invoiceService;
private readonly ICustomerService _customerService;
private readonly ILogger<InvoiceController> _logger;
// Constructor injection for testability
public InvoiceController(
IInvoiceService invoiceService,
ICustomerService customerService,
ILogger<InvoiceController> logger)
{
_invoiceService = invoiceService;
_customerService = customerService;
_logger = logger;
}
// Required outlets for critical template elements
[PDFOutlet(Required = true)]
public Label InvoiceNumberLabel { get; set; }
[PDFOutlet(Required = true)]
public ForEach LineItemsRepeater { get; set; }
// Optional outlets with fallback handling
[PDFOutlet]
public Label CustomerNotesLabel { get; set; }
// Clear, single-purpose action methods
public void InitInvoice(InitContext context)
{
_logger.LogInformation("Initializing invoice document");
try
{
var invoiceId = context.Document.Params["InvoiceId"] as string;
var invoice = _invoiceService.GetInvoice(invoiceId);
InvoiceNumberLabel.Text = $"Invoice #{invoice.Number}";
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize invoice");
throw;
}
}
public void LoadInvoiceData(LoadContext context)
{
_logger.LogInformation("Loading invoice data");
try
{
var invoiceId = context.Document.Params["InvoiceId"] as string;
var invoice = _invoiceService.GetInvoiceWithDetails(invoiceId);
// Transform for display (presentation concern)
var lineItems = invoice.LineItems.Select(li => new
{
Description = li.Description,
Quantity = li.Quantity,
UnitPrice = li.UnitPrice.ToString("C"),
Total = (li.Quantity * li.UnitPrice).ToString("C")
}).ToList();
LineItemsRepeater.DataSource = lineItems;
LineItemsRepeater.Value = lineItems;
// Handle optional outlet
if (CustomerNotesLabel != null && !string.IsNullOrEmpty(invoice.Notes))
{
CustomerNotesLabel.Text = invoice.Notes;
}
_logger.LogInformation($"Loaded {lineItems.Count} line items");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load invoice data");
throw;
}
}
}
Outlet Naming
β Do:
- Use descriptive names:
CustomerNameLabelnotLabel1 - Match outlet names to template IDs for clarity
- Use consistent naming conventions across controllers
- Suffix with component type:
...Label,...Repeater,...Panel
β Donβt:
- Use cryptic abbreviations:
CustNmLbl - Mix naming conventions in same controller
- Use generic names:
Data,Content,Item
Custom Components
Component Design Principles
Single Responsibility:
// β
Good: Focused component
[PDFParsableComponent("StatCard")]
public class StatCard : Panel
{
[PDFAttribute("value")]
public string Value { get; set; }
[PDFAttribute("label")]
public string Label { get; set; }
}
// β Bad: Kitchen sink component
[PDFParsableComponent("Dashboard")]
public class Dashboard : Panel
{
// Too many responsibilities:
// - Data loading
// - Chart rendering
// - Table generation
// - Export functionality
}
Composability:
// β
Good: Composable components
<custom:DashboardCard title='Sales'>
<Sections>
<section>
<custom:StatCard value='$125K' label='Revenue' />
</section>
<section>
<custom:LineChart data='{@:SalesData}' />
</section>
</Sections>
</custom:DashboardCard>
// β Bad: Monolithic component
<custom:SalesDashboard
revenue='$125K'
chartData='...'
tableData='...'
showExport='true' />
Lifecycle Usage:
protected override void OnInit(InitContext context)
{
base.OnInit(context);
// β
Do: Build child content
BuildCardContent();
// β Don't: Load external data (use OnLoad)
// var data = _httpClient.GetAsync("...").Result;
}
protected override void OnLoad(LoadContext context)
{
base.OnLoad(context);
// β
Do: Load external data, access services
var service = context.ServiceProvider.GetService<IDataService>();
var data = service.LoadData();
}
Property Design
β Do:
- Provide sensible defaults
- Support common data types (string, int, bool, enum)
- Validate property values
- Document property purposes
β Donβt:
- Require all properties
- Use complex object types for attributes
- Throw in property setters
- Add properties βjust in caseβ
Example:
[PDFParsableComponent("ProgressBar")]
public class ProgressBar : Panel
{
// Sensible default
[PDFAttribute("percentage")]
public double Percentage { get; set; } = 0;
// Validation in lifecycle method
protected override void OnInit(InitContext context)
{
base.OnInit(context);
// Validate and clamp
if (Percentage < 0) Percentage = 0;
if (Percentage > 100) Percentage = 100;
BuildProgressBar();
}
}
Namespace Registration
Namespace URI Design
β Do:
- Use your company domain:
http://mycompany.com/schemas/components - Include version for breaking changes:
http://mycompany.com/schemas/v2/components - Group related components:
http://mycompany.com/components/charts - Document namespace URIs in README
β Donβt:
- Use
http://localhostorhttp://example.com - Change URIs without versioning
- Use generic names:
http://components - Mix unrelated components in same namespace
Assembly Organization
β Good Structure:
MyCompany.Components/
βββ Cards/
β βββ StatCard.cs
β βββ ProductCard.cs
βββ Charts/
β βββ BarChart.cs
β βββ LineChart.cs
βββ Forms/
βββ TextField.cs
βββ Checkbox.cs
Configuration:
{
"Namespaces": [
{
"XMLNamespace": "http://mycompany.com/components/cards",
"AssemblyPrefix": "MyCompany.Components.Cards, MyCompany.Components"
},
{
"XMLNamespace": "http://mycompany.com/components/charts",
"AssemblyPrefix": "MyCompany.Components.Charts, MyCompany.Components"
}
]
}
Image Factories
Factory Design
β Do:
- Use specific, non-overlapping patterns
- Handle errors gracefully with placeholders
- Dispose resources properly
- Make factories thread-safe
- Add caching when appropriate
- Log factory operations
β Donβt:
- Use overly broad patterns:
.* - Throw exceptions without context
- Block on synchronous I/O in production
- Ignore thread safety
- Cache unbounded amounts of data
Pattern Design
// β
Good: Specific, unambiguous pattern
public string FactoryKey => @"^product://[\w-]+$";
// β Bad: Overly broad pattern
public string FactoryKey => @".*";
// β
Good: Protocol-based patterns
public string FactoryKey => @"^db://[\w/]+$"; // Database
public string FactoryKey => @"^s3://[\w/]+$"; // S3
public string FactoryKey => @"^azblob://[\w/]+$"; // Azure Blob
Error Handling
public byte[] LoadImageData(IDocument document, IComponent component, string source)
{
try
{
return LoadFromSource(source);
}
catch (FileNotFoundException ex)
{
_logger.LogWarning(ex, $"Image not found: {source}");
return GetPlaceholderImage(); // Graceful degradation
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to load image: {source}");
throw new PDFImageException($"Image load failed: {source}", ex);
}
}
Font Configuration
Font Selection
β Do:
- Choose fonts with complete character sets
- Test with expected character ranges (including special characters)
- Provide fallback fonts
- Document font licenses
- Use web-safe fonts as last resort
β Donβt:
- Assume fonts support all Unicode characters
- Use unlicensed fonts in production
- Forget to register all font weights/styles
- Use excessive number of font families
Font Organization
MyApp/
βββ wwwroot/
βββ fonts/
βββ Roboto/
β βββ Roboto-Regular.ttf
β βββ Roboto-Bold.ttf
β βββ Roboto-Italic.ttf
β βββ Roboto-BoldItalic.ttf
βββ OpenSans/
β βββ OpenSans-Regular.ttf
β βββ OpenSans-Bold.ttf
βββ LICENSE.txt # Font licenses
Font Fallback
<html>
<head>
<style>
body {
/* Primary β Fallback 1 β Fallback 2 β Generic */
font-family: Roboto, 'Open Sans', Helvetica, sans-serif;
}
</style>
</head>
</html>
Performance Optimization
Document Generation
β Do:
- Use async/await for I/O operations
- Cache frequently used data
- Lazy-load images and fonts
- Monitor memory usage for large documents
- Use streaming for large output files
β Donβt:
- Load entire databases into memory
- Generate PDFs on UI thread
- Create excessive objects during layout
- Ignore memory leaks
- Block on synchronous I/O
Example: Efficient Data Loading
public class ReportController
{
private readonly IReportService _reportService;
private readonly IMemoryCache _cache;
public async Task LoadReportDataAsync(LoadContext context)
{
var cacheKey = $"report-{context.Document.Params["ReportId"]}";
// Check cache first
if (!_cache.TryGetValue(cacheKey, out ReportData data))
{
// Load from database
data = await _reportService.GetReportDataAsync(reportId);
// Cache for 5 minutes
_cache.Set(cacheKey, data, TimeSpan.FromMinutes(5));
}
// Bind to template
DataRepeater.DataSource = data.Items;
}
}
Component Performance
// β
Good: Efficient child creation
protected override void OnInit(InitContext context)
{
base.OnInit(context);
// Create children once
BuildContent();
}
// β Bad: Recreating children on every layout
protected override void OnPreLayout(LayoutContext context)
{
base.OnPreLayout(context);
// Don't recreate children in layout phase!
this.Contents.Clear();
BuildContent();
}
Error Handling
Processing Instruction Modes
Development:
<?scryber parser-mode='Lax'
parser-log='true'
append-log='true'
log-level='Diagnostic' ?>
Production:
<?scryber parser-mode='Strict'
log-level='Warnings' ?>
Validation
β Do:
- Validate early (in Init phase)
- Provide descriptive error messages
- Log errors with context
- Fail fast on configuration errors
- Handle optional outlets gracefully
β Donβt:
- Swallow exceptions silently
- Throw generic exceptions
- Continue processing after critical errors
- Log sensitive data
Example: Robust Controller
public void InitReport(InitContext context)
{
try
{
// Validate document parameters
if (!context.Document.Params.ContainsKey("ReportId"))
{
throw new PDFControllerException(
"Required parameter 'ReportId' not provided",
nameof(InitReport));
}
// Validate outlets
if (TitleLabel == null)
{
throw new PDFControllerException(
"Required outlet 'TitleLabel' not found in template",
nameof(InitReport));
}
// Initialize
var reportId = context.Document.Params["ReportId"] as string;
_logger.LogInformation($"Initializing report: {reportId}");
TitleLabel.Text = $"Report {reportId}";
}
catch (PDFControllerException)
{
// Re-throw controller exceptions
throw;
}
catch (Exception ex)
{
// Wrap other exceptions
_logger.LogError(ex, "Report initialization failed");
throw new PDFControllerException(
"Failed to initialize report",
nameof(InitReport),
ex);
}
}
Testing
Unit Testing Controllers
[TestClass]
public class InvoiceControllerTests
{
[TestMethod]
public void InitInvoice_SetsInvoiceNumber()
{
// Arrange
var mockService = new Mock<IInvoiceService>();
mockService.Setup(s => s.GetInvoice("12345"))
.Returns(new Invoice { Number = "INV-12345" });
var controller = new InvoiceController(mockService.Object);
controller.InvoiceNumberLabel = new Label();
var context = new InitContext(new Document(), new TraceLog(TraceRecordLevel.Off), null);
context.Document.Params["InvoiceId"] = "12345";
// Act
controller.InitInvoice(context);
// Assert
Assert.AreEqual("Invoice #INV-12345", controller.InvoiceNumberLabel.Text);
}
}
Integration Testing Templates
[TestMethod]
public void GenerateInvoice_ProducesValidPDF()
{
// Arrange
var service = new InvoiceService(connectionString);
var controller = new InvoiceController(service);
var settings = new ParserSettings();
settings.Controller = controller;
// Act
using (var reader = new StreamReader("Invoice.pdfx"))
using (var output = new MemoryStream())
{
var doc = Document.ParseDocument(reader, ParseSourceType.DynamicContent, settings);
doc.ProcessDocument(output);
// Assert
Assert.IsTrue(output.Length > 0);
Assert.IsTrue(output.ToArray().StartsWith(new byte[] { 0x25, 0x50, 0x44, 0x46 })); // %PDF
}
}
Security
Configuration Security
β Do:
- Use Azure Key Vault, AWS Secrets Manager for secrets
- Encrypt connection strings
- Use environment variables for sensitive data
- Implement least-privilege access
- Rotate credentials regularly
β Donβt:
- Store credentials in source control
- Hard-code secrets
- Use same credentials across environments
- Log sensitive data
Input Validation
public void LoadCustomerData(LoadContext context)
{
// Validate and sanitize inputs
var customerId = context.Document.Params["CustomerId"] as string;
// β
Validate format
if (string.IsNullOrWhiteSpace(customerId))
throw new ArgumentException("Customer ID is required");
// β
Sanitize for SQL
if (!Regex.IsMatch(customerId, @"^[A-Z0-9-]+$"))
throw new ArgumentException("Invalid customer ID format");
// β
Use parameterized queries
var customer = _service.GetCustomer(customerId); // Parameterized internally
}
Documentation
Component Documentation
/// <summary>
/// Displays a metric card with value, label, and optional trend indicator.
/// </summary>
/// <example>
/// <code>
/// <custom:StatCard value='$125,432'
/// label='Total Sales'
/// trend='+12.5%'
/// trend-positive='true' />
/// </code>
/// </example>
[PDFParsableComponent("StatCard")]
public class StatCard : Panel
{
/// <summary>
/// Gets or sets the main value to display (e.g., "$125,432").
/// </summary>
[PDFAttribute("value")]
public string Value { get; set; }
/// <summary>
/// Gets or sets the descriptive label (e.g., "Total Sales").
/// </summary>
[PDFAttribute("label")]
public string Label { get; set; }
}
Template Documentation
<!--
Product Catalog Template
Parameters:
- CatalogId: string - Catalog identifier (required)
- IncludeOutOfStock: bool - Include out-of-stock products (optional, default: false)
Controller: ProductCatalog.Controllers.CatalogController
Custom Components:
- prod:ProductCard - Displays individual product with image, pricing, rating
- prod:RatingStars - Displays star rating visualization
Image Sources:
- db://ProductImages/{productId} - Loads from ProductImages table
Fonts:
- Roboto: Headers and product names
- Open Sans: Body text and descriptions
-->
<?xml version='1.0' encoding='utf-8' ?>
<?scryber controller='ProductCatalog.Controllers.CatalogController, ProductCatalog' ?>
<html xmlns='http://www.w3.org/1999/xhtml'>
<!-- Template content -->
</html>
Deployment
Checklist
Pre-Deployment:
- Switch to
parser-mode='Strict' - Set
log-level='Warnings'or'Errors' - Disable
append-log - Remove verbose logging
- Test with production-like data volumes
- Verify font licensing for production
- Check image factory error handling
- Validate connection strings
- Test error scenarios
- Review security settings
Deployment:
- Deploy configuration files
- Deploy font files
- Deploy custom assemblies
- Configure logging/monitoring
- Set up health checks
- Configure PDF storage location
- Test in production environment
Post-Deployment:
- Monitor error logs
- Check PDF generation performance
- Verify PDF output quality
- Monitor memory usage
- Test user-facing workflows
Common Pitfalls
Pitfall 1: Forgetting Base Calls
// β Bad: Missing base call
protected override void OnInit(InitContext context)
{
BuildContent(); // Base.OnInit not called!
}
// β
Good: Call base first
protected override void OnInit(InitContext context)
{
base.OnInit(context);
BuildContent();
}
Pitfall 2: Modifying During Layout
// β Bad: Changing structure during layout
protected override void OnPreLayout(LayoutContext context)
{
base.OnPreLayout(context);
this.Contents.Add(new Label()); // Can cause layout issues
}
// β
Good: Build structure in Init
protected override void OnInit(InitContext context)
{
base.OnInit(context);
this.Contents.Add(new Label()); // Correct phase
}
Pitfall 3: Blocking on Async
// β Bad: Blocking async call
public void LoadData(LoadContext context)
{
var data = _httpClient.GetAsync("...").Result; // Deadlock risk!
}
// β
Good: Use sync version or refactor
public void LoadData(LoadContext context)
{
var data = _httpClient.GetStringAsync("...").GetAwaiter().GetResult();
// Or better: Make service provide sync API
}
Pitfall 4: Ignoring Disposal
// β Bad: Resource leak
public byte[] LoadImageData(...)
{
var connection = new SqlConnection(_connectionString);
connection.Open();
// connection never disposed!
return LoadFromDatabase(connection);
}
// β
Good: Proper disposal
public byte[] LoadImageData(...)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
return LoadFromDatabase(connection);
}
}
Summary
Quick Reference
| Topic | Key Guideline |
|---|---|
| Configuration | Use environment-specific files, externalize secrets |
| Controllers | Inject dependencies, keep focused, log operations |
| Components | Single responsibility, composable, sensible defaults |
| Namespaces | Use domain-based URIs, version for breaking changes |
| Image Factories | Specific patterns, graceful errors, thread-safe |
| Fonts | Test character sets, document licenses, provide fallbacks |
| Performance | Cache data, lazy-load resources, monitor memory |
| Error Handling | Validate early, log with context, fail fast |
| Testing | Unit test controllers, integration test templates |
| Security | Vault secrets, validate inputs, parameterized queries |
| Documentation | Document components, templates, configuration |
| Deployment | Strict mode, minimal logging, test thoroughly |