C# and .NET Customization
Treat the generated project as a replaceable domain API. Put application services, dependency injection, authenticated context construction, policies, and external input adapters in handwritten projects.
Register infrastructure once
Use the host's dependency injection container for long-lived provider resources and a
scoped factory for UserContext:
public sealed record TrustedRequest(
long TenantId,
string ActorId,
IReadOnlySet<string> Permissions);
public sealed class TeaqlContextFactory
{
public Task<UserContext> CreateAsync(TrustedRequest trusted)
{
var context = new UserContext();
// Register generated metadata, provider, policy and two audit sinks.
// Add trusted identity/authorization; never copy them from body JSON.
return Task.FromResult(context);
}
}
Use released registration APIs after publication. The architectural invariant is one context argument at every execution/save terminal.
The current runtime composition shape is:
public static UserContext RequestContext(
RuntimeModule module,
IDataService provider,
IRequestPolicy requestPolicy,
string trustedTenant,
IAppAuditEventSink appAuditSink,
IRuntimeTelemetry telemetry)
{
if (string.IsNullOrWhiteSpace(trustedTenant))
throw new ArgumentException("trusted tenant is required", nameof(trustedTenant));
return module.IntoContext()
.WithDataService(provider)
.WithRequestPolicy(requestPolicy)
.WithAppAuditEventSink(appAuditSink)
.WithTrustedTenant(trustedTenant)
.WithRuntimeTelemetry(telemetry);
}
public static Task ReadinessAsync(UserContext context) =>
context.EnsureSchemaAsync();
The general IRequestPolicy prepares queries. Enforce Mutation tenant
authorization at the application/provider/TFP boundary as well.
Map dynamic input explicitly
Deserialize into a narrow transport DTO, validate limits and types, then switch over allowlisted fields/operators/sorts. Do not use reflection to turn user strings into generated method calls and never interpolate application SQL to fill an API gap.
request = filter switch
{
{ Field: "order_number", Operator: "contains" } =>
request.WithOrderNumberContaining(RequireString(filter.Value)),
_ => throw new InvalidQueryException("Unsupported field or operator")
};
Tenant, merchant, actor, permissions, provider, request policy and purpose policy are always server-owned.
Domain behavior versus platform policy
Use generated behavior/checker contracts for entity-specific validation, normalization and lifecycle rules. Use context-level request policy for tenant scope, authorization, approved purposes, paging caps, and raw-query restrictions. Register both at application startup and test through generated Requests.
Audit sinks
AuditAs declares mutation reason. Preserve the runtime row/mutation audit path, then
optionally register an application sink for an outbox, compliance stream, or telemetry.
The application event must retain trusted actor attribution and masking. It does not
replace immutable runtime audit.
Provider and transaction boundary
Configure connection factories/pools through DI and context assembly. Keep provider selection out of controllers and client JSON. Test transaction rollback, optimistic locking, reconnect, native aggregates, stable pagination and relation Top-N for every provider claimed in deployment documentation.
Packaging after release
Pin official NuGet packages centrally, commit the lock file where applicable, and test restore/build in a clean environment. Until official coordinates exist, leave the installation command blank rather than publishing an unverifiable package name.
If customization requires an edit in generated Q.cs, E.cs, Requests, or Models,
move it to KSML, the generator, or a runtime extension point and regenerate.
Runtime infrastructure
.NET UserContext provides process-local cache and keyed locks plus
IRemoteCacheProvider/IRemoteLockProvider. The Redis package uses
StackExchange.Redis; the current lock release performs a non-atomic
get-then-delete, so it is not equivalent to owner-safe compare-and-delete.
TeaQL currently claims no .NET Nacos or Consul adapter. See
Cache, Lock, and Cloud Runtime Infrastructure
and include required provider health in ASP.NET readiness.
Worked Example: Local Cache and Lock
public static async Task<CurrencyView> CurrencyViewAsync(
UserContext context, string tenant, string code)
{
var cacheKey = $"{tenant}:currency:{code}";
var cached = context.GetFromLocalCache<CurrencyView>(cacheKey);
if (cached is not null) return cached;
var lockKey = $"{tenant}:currency-refresh:{code}";
if (!context.TryLocalLock(lockKey, timeoutMillis: 250, expireMillis: 5_000))
throw new InvalidOperationException("currency refresh is busy");
try
{
cached = context.GetFromLocalCache<CurrencyView>(cacheKey);
if (cached is not null) return cached;
var value = await LoadAuthorizedCurrencyViewAsync(context, code);
context.PutToLocalCache(cacheKey, value, timeToLiveInSeconds: 300);
return value;
}
finally
{
context.UnlockLocal(lockKey);
}
}
Remove the cache entry after the audited Mutation commits. For a multi-instance critical section, use an owner-safe atomic Remote Lock provider and fail readiness when it is missing; the current Redis adapter's get-then-delete release is not sufficient as the only guard.