llblgen-pro-rtf

star 0

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 LLBLGe

djdd87 By djdd87 schedule Updated 2/28/2026

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

  1. DataAccessAdapter is not thread-safe. Create a new instance per operation — it's practically free.
  2. Async re-entrance. Always await before calling another method on the same adapter.
  3. N+1 queries. Use prefetch paths. LLBLGen doesn't lazy-load — unloaded navigation properties return empty collections.
  4. Always using on DataAccessAdapter. Ensures connections release and transactions rollback.
  5. Direct updates/deletes bypass validation, auditing, and authorization. Use fetch-modify-save if you need those.
  6. ParameterisedPrefetchPathThreshold too high. Keep under 300 unless profiling proves otherwise.
  7. 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 LeftJoin and RightJoin operators (.NET 10) now supported
  • PostgreSQL DateOnly and TimeOnly support
Install via CLI
npx skills add https://github.com/djdd87/llblgen-pro-rtf-skill --skill llblgen-pro-rtf
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator