name: llblgen-pro-rtf description: "Use this skill whenever the user asks about LLBLGen Pro Runtime Framework (RTF), its ORM patterns, or generated code. Triggers include: any mention of 'LLBLGen', 'LLBLGen Pro', 'DataAccessAdapter', 'EntityCollection', 'QuerySpec', 'QueryFactory', 'PrefetchPath', 'RelationPredicateBucket', 'UnitOfWork2', 'EntityField2', 'TypedList', 'TypedView', 'DynamicQuery', 'EntityQuery', 'PredicateExpression', or references to LLBLGen-specific patterns like prefetch paths, adapter pattern fetching, or LLBLGen filtering/predicates. Also trigger when the user is writing C# code that uses LLBLGen entity classes (e.g. classes ending in 'Entity' with Fields classes like 'CustomerFields'), or when asking about ORM query patterns that involve the LLBLGen low-level API, QuerySpec, or the LLBLGen Linq provider. Even if the user just says 'how do I query' or 'how do I save' in the context of LLBLGen entities, use this skill. Do NOT use for Entity Framework, Dapper, NHibernate, or other ORM frameworks unless comparing them to LLBLGen."
LLBLGen Pro Runtime Framework v5.13 — Adapter Template Group
This skill covers the LLBLGen Pro Runtime Framework (RTF) using the Adapter template group with C#. The official documentation is at: https://www.llblgen.com/Documentation/5.13/LLBLGen%20Pro%20RTF/index.htm
Query API reference files (read these when the user specifically asks about these APIs):
references/queryspec.md— QuerySpec fluent API (QueryFactory,EntityQuery<T>,DynamicQuery)references/linq.md— Linq to LLBLGen Pro
Core Architecture
LLBLGen Pro Adapter generates two projects:
- Database Generic project: Entity classes, typed views/lists, field classes, relation classes — database-agnostic
- Database Specific (DbSpecific) project:
DataAccessAdapter, stored procedure classes — database-specific
DataAccessAdapter is the single class for all database interaction. It is not thread-safe — create a new instance per operation (creation is practically free, almost zero overhead). Always wrap in a using block.
Fetching Entities
Single Entity by Primary Key
CustomerEntity customer = new CustomerEntity("CHOPS");
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
bool found = adapter.FetchEntity(customer);
}
Single Entity with Prefetch Path
CustomerEntity customer = new CustomerEntity("CHOPS");
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders);
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntity(customer, path);
}
Entity Collection with Filter
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
RelationPredicateBucket filter = new RelationPredicateBucket(
CustomerFields.Country == "Germany");
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers, filter);
}
Entity Collection with Filter, Sort, and Paging
EntityCollection<OrderEntity> orders = new EntityCollection<OrderEntity>();
RelationPredicateBucket filter = new RelationPredicateBucket(
OrderFields.OrderDate >= new DateTime(2024, 1, 1));
SortExpression sort = new SortExpression(
OrderFields.OrderDate | SortOperator.Descending);
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
// Fetch page 2, 25 rows per page
adapter.FetchEntityCollection(orders, filter, 25, sort, null, 2, 25);
}
Fetching Related Entities into Existing Collection
// Fetch orders for an already-loaded customer using the helper method
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(
myCustomer.Orders,
myCustomer.GetRelationInfoOrders());
}
Filtering and Predicates
LLBLGen Pro uses predicate classes built from EntityField2 objects. The generated {EntityName}Fields classes provide typed field access.
Building Predicates
// Comparison operators work directly on fields
CustomerFields.Country == "Germany"
OrderFields.OrderDate >= new DateTime(2024, 1, 1)
OrderFields.Freight > 50m
// PredicateExpression for combining
PredicateExpression filter = new PredicateExpression();
filter.Add(CustomerFields.Country == "Germany");
filter.AddWithAnd(CustomerFields.City == "Berlin");
// Or combine inline
IPredicate combined = (CustomerFields.Country == "Germany")
.And(CustomerFields.City == "Berlin");
IPredicate either = (CustomerFields.Country == "Germany")
.Or(CustomerFields.Country == "France");
Common Predicate Patterns
// String operations
CustomerFields.CompanyName.Like("A%")
// NULL checks
CustomerFields.Region == System.DBNull.Value // IS NULL
CustomerFields.Region != System.DBNull.Value // IS NOT NULL
// IN clause
OrderFields.ShipCountry.In(new[] { "USA", "UK", "Germany" })
// BETWEEN
OrderFields.Freight.Between(10m, 50m)
// Negation
new PredicateExpression(CustomerFields.Country == "Germany").Negate()
// Aggregate predicates (for HAVING)
IPredicate havingFilter = OrderDetailFields.Quantity
.SetAggregateFunction(AggregateFunctions.Sum) > 100;
SQL-to-LLBLGen Predicate Quick Reference
| SQL | LLBLGen Predicate |
|---|---|
field = value |
EntityFields.Field == value |
field LIKE pattern |
EntityFields.Field.Like(pattern) |
field IN (values) |
EntityFields.Field.In(values) |
field IN (SELECT...) |
FieldCompareSetPredicate or QuerySpec .In() |
field BETWEEN a AND b |
EntityFields.Field.Between(a, b) |
field IS NULL |
EntityFields.Field == DBNull.Value |
EXISTS (SELECT...) |
FieldCompareSetPredicate with SetOperator.Exist |
A AND B |
predA.And(predB) or .AddWithAnd() |
A OR B |
predA.Or(predB) or .AddWithOr() |
Multi-Entity Filters (Joins)
When filtering on fields from related entities, add relationships to the RelationPredicateBucket.
// Fetch customers who have orders shipped by employee 4
RelationPredicateBucket filter = new RelationPredicateBucket();
filter.Relations.Add(CustomerEntity.Relations.OrderEntityUsingCustomerId);
filter.PredicateExpression.Add(OrderFields.EmployeeId == 4);
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers, filter);
}
Multi-Level Join Chain
// Customers who bought products from French suppliers
RelationPredicateBucket filter = new RelationPredicateBucket();
filter.Relations.Add(CustomerEntity.Relations.OrderEntityUsingCustomerId);
filter.Relations.Add(OrderEntity.Relations.OrderDetailEntityUsingOrderId);
filter.Relations.Add(OrderDetailEntity.Relations.ProductEntityUsingProductId);
filter.Relations.Add(ProductEntity.Relations.SupplierEntityUsingSupplierId);
filter.PredicateExpression.Add(SupplierFields.Country == "France");
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers, filter);
}
Aliased Joins (Same Entity Joined Multiple Times)
// Customers with visiting address in Amsterdam AND billing address in Rotterdam
RelationPredicateBucket filter = new RelationPredicateBucket();
filter.Relations.Add(
CustomerEntity.Relations.AddressEntityUsingVisitingAddressId, "VA");
filter.Relations.Add(
CustomerEntity.Relations.AddressEntityUsingBillingAddressId, "BA");
filter.PredicateExpression.Add(
AddressFields.City.SetObjectAlias("VA") == "Amsterdam");
filter.PredicateExpression.AddWithAnd(
AddressFields.City.SetObjectAlias("BA") == "Rotterdam");
Custom Filter on JOIN (ON clause extension)
IEntityRelation relation = CustomerEntity.Relations.OrderEntityUsingCustomerId;
PredicateExpression customFilter = new PredicateExpression();
customFilter.Add(OrderFields.ShipCountry == "Mexico");
filter.Relations.Add(relation).CustomFilter = customFilter;
Prefetch Paths (Eager Loading)
Prefetch paths fetch related entities efficiently — one query per node in the path. This avoids N+1 problems.
Single Level
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders);
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers, null, path);
}
Multi-Level with Sub-Paths
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders)
.SubPath.Add(OrderEntity.PrefetchPathOrderDetails);
path.Add(CustomerEntity.PrefetchPathVisitingAddress);
// Executes 4 queries: Customers, Orders, OrderDetails, Addresses
Filtered, Sorted, Limited Prefetch Nodes
PrefetchPath2 path = new PrefetchPath2(EntityType.EmployeeEntity);
// Fetch only the last 5 orders per employee, sorted by date
IPrefetchPathElement2 ordersNode = path.Add(EmployeeEntity.PrefetchPathOrders);
ordersNode.Filter = new RelationPredicateBucket(
OrderFields.OrderDate >= new DateTime(2024, 1, 1));
ordersNode.Sorter = new SortExpression(
OrderFields.OrderDate | SortOperator.Descending);
ordersNode.MaxNumberOfItemsToReturn = 5;
Exclude/Include Fields on Prefetch Nodes
IPrefetchPathElement2 ordersNode = path.Add(CustomerEntity.PrefetchPathOrders);
ExcludeIncludeFieldsList excludeFields = new ExcludeIncludeFieldsList(true);
excludeFields.Add(OrderFields.ShipAddress);
excludeFields.Add(OrderFields.ShipCity);
ordersNode.ExcludedIncludedFields = excludeFields;
ParameterisedPrefetchPathThreshold
Controls whether prefetch sub-queries use IN clauses or full subqueries based on parent entity count. Set per adapter instance. Keep under 300 unless profiling shows benefit.
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.ParameterisedPrefetchPathThreshold = 200;
adapter.FetchEntityCollection(customers, null, path);
}
Creating (Insert)
CustomerEntity newCustomer = new CustomerEntity();
newCustomer.CustomerId = "NEWCO";
newCustomer.CompanyName = "New Company Ltd";
newCustomer.Country = "UK";
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.SaveEntity(newCustomer); // INSERT — entity.IsNew is true
}
Updating
// Option 1: Fetch-modify-save (triggers validation/auditing)
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
CustomerEntity customer = new CustomerEntity("CHOPS");
adapter.FetchEntity(customer);
customer.Phone = "(605)555-4321";
adapter.SaveEntity(customer); // UPDATE — only changed fields
}
// Option 2: Direct update (no fetch, bypasses validation/auditing)
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
CustomerEntity updateBucket = new CustomerEntity();
updateBucket.Phone = "(605)555-4321";
adapter.UpdateEntitiesDirectly(updateBucket,
new RelationPredicateBucket(CustomerFields.Country == "Germany"));
}
Deleting
// Option 1: Fetch then delete (triggers validation/auditing)
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
CustomerEntity customer = new CustomerEntity("CHOPS");
adapter.FetchEntity(customer);
adapter.DeleteEntity(customer);
}
// Option 2: Direct delete (no fetch, bypasses validation/auditing)
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
int deleted = adapter.DeleteEntitiesDirectly(
typeof(ProductEntity),
new RelationPredicateBucket(ProductFields.Discontinued == true));
}
Recursive Saves
SaveEntity() performs recursive saves by default — traverses the entity graph and saves all new/dirty entities in correct FK order, syncing PK-FK values automatically. Disable with adapter.SaveEntity(entity, false).
Saving/Deleting Collections
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
// Saves all dirty entities in the collection (auto-transactioned)
adapter.SaveEntityCollection(customers);
// Deletes all entities in the collection from the database
adapter.DeleteEntityCollection(ordersToRemove);
}
Transactions
Adapter auto-wraps recursive saves and collection operations in transactions. For explicit control:
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.StartTransaction(IsolationLevel.ReadCommitted, "MyTransaction");
try
{
adapter.SaveEntity(customer);
adapter.SaveEntity(order);
adapter.Commit();
}
catch
{
adapter.Rollback();
throw;
}
}
Adapter rolls back any open transaction on dispose. Transaction save-points are supported for partial rollback via adapter.SaveTransaction("savePointName") and adapter.Rollback("savePointName").
UnitOfWork2
Collects entity save/delete actions and executes them atomically. Automatically determines order: Inserts → Updates → Deletes (configurable).
UnitOfWork2 uow = new UnitOfWork2();
uow.AddForSave(newCustomer, true); // true = refetch after save
uow.AddForDelete(productToDelete);
uow.AddCollectionForDelete(order.OrderDetails);
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
uow.Commit(adapter, true); // true = auto-commit transaction
}
Multiple UnitOfWork2 objects can share a transaction by passing the same adapter with autoCommit: false.
Async/Await
Every synchronous method has an async equivalent: {Method}Async. Always await before reusing the adapter — it is not thread-safe.
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
RelationPredicateBucket filter = new RelationPredicateBucket(
CustomerFields.Country == "Germany");
await adapter.FetchEntityCollectionAsync(customers, filter, CancellationToken.None);
// Safe: previous operation completed
CustomerEntity single = new CustomerEntity("CHOPS");
await adapter.FetchEntityAsync(single, CancellationToken.None);
}
// NEVER: Task t = adapter.FetchAsync(...); adapter.Fetch(...); await t;
Key async methods: FetchEntityAsync, FetchEntityCollectionAsync, SaveEntityAsync, SaveEntityCollectionAsync, DeleteEntityAsync, DeleteEntitiesDirectlyAsync, UpdateEntitiesDirectlyAsync.
Dependency Injection & RuntimeConfiguration
Configure in Program.cs / Startup.Configure() — must run before any LLBLGen Pro usage.
// Connection string (required for .NET Core / .NET 5+)
RuntimeConfiguration.AddConnectionString(
"ConnectionString.SQL Server (SqlClient)", connectionString);
// DQE configuration
RuntimeConfiguration.ConfigureDQE<SQLServerDQEConfiguration>(
c => c.SetDefaultCompatibilityLevel(SqlServerCompatibilityLevel.SqlServer2012)
.AddDbProviderFactory(typeof(System.Data.SqlClient.SqlClientFactory)));
// DI for validators/authorizers (v5.8+)
RuntimeConfiguration.SetDependencyInjectionInfo(
new[] { typeof(MyValidator).Assembly }, null);
Batching (v5.5+)
Batch inserts/updates into single DbCommand objects for performance:
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.BatchSize = 50;
adapter.SaveEntityCollection(largeCollection);
}
Transient Error Recovery (v5.2+)
RuntimeConfiguration.ConfigureDQE<SQLServerDQEConfiguration>(
c => c.SetTransientErrorRecoveryStrategy(new SqlAzureRecoveryStrategy()));
Common Pitfalls
- DataAccessAdapter is not thread-safe. Create a new instance per operation — it's practically free.
- Async re-entrance. Always
awaitbefore calling another method on the same adapter. - N+1 queries. Use prefetch paths. LLBLGen doesn't lazy-load — unloaded navigation properties return empty collections.
- Always
usingon DataAccessAdapter. Ensures connections release and transactions rollback. - Direct updates/deletes bypass validation, auditing, and authorization. Use fetch-modify-save if you need those.
ParameterisedPrefetchPathThresholdtoo high. Keep under 300 unless profiling proves otherwise.- FK constraint violations during recursive saves. Check entity graph for circular references or missing relationship definitions.
Documentation Reference
Full v5.13 docs: https://www.llblgen.com/Documentation/5.13/LLBLGen%20Pro%20RTF/index.htm
v5.13 Specifics
- .NET 10 support
- Linq
LeftJoinandRightJoinoperators (.NET 10) now supported - PostgreSQL
DateOnlyandTimeOnlysupport