Polyglot Descriptors
The ontology layer originally keyed object types by their CLR Type. That breaks when the type lives in another runtime — a TypeScript service, a Python pipeline, or anything indexed via SCIP. Polyglot descriptors (2.5.0, #48) let an ObjectTypeDescriptor identify itself by SymbolKey instead of (or alongside) ClrType. This page covers the shape, when to use which field, and how AONT037 catches missing identity at build time.
The shape
Section titled “The shape”ObjectTypeDescriptor lives in Strategos.Ontology.Descriptors and now carries four identity-related fields:
public sealed record ObjectTypeDescriptor{ public required string Name { get; init; } public required string DomainName { get; init; }
public Type? ClrType { get; init; } // nullable for non-.NET descriptors public string? SymbolKey { get; init; } // SCIP moniker public string? SymbolFqn { get; init; } // language-formatted FQN; informational public string LanguageId { get; init; } = "dotnet";
public DescriptorSource Source { get; init; } = DescriptorSource.HandAuthored; public string? SourceId { get; init; } public DateTimeOffset? IngestedAt { get; init; }
// ... existing Properties, Links, Actions, Events, Lifecycle ...}The construction invariant: at least one of ClrType and SymbolKey must be non-null. Setting SymbolKey while both fields are still null throws InvalidOperationException.
SymbolKey is the SCIP moniker — a stable, language-agnostic identifier. LanguageId carries the SCIP language tag ("dotnet", "typescript", "python", …) and defaults to "dotnet" so existing hand-authored code compiles unchanged. Source is HandAuthored for DomainOntology.Define() contributions and Ingested when an IOntologySource produced the descriptor; SourceId and IngestedAt carry provenance for the ingested case.
Which field do I set?
Section titled “Which field do I set?”The hand-authored DSL builder.Object<T>(...) populates ClrType from the type parameter and leaves SymbolKey null. You only think about polyglot identity when:
- Contributing descriptors through
IOntologySourcefrom a non-.NET runtime — setSymbolKeyandLanguageId, leaveClrTypenull. - Using the descriptor-by-name overload
builder.ObjectType("Name", domainName: "...")for a shape with no loaded .NET type — supply either aTypeor asymbolKey:named argument.
The merge lattice: ClrType hand wins (falls back to ingested); SymbolKey ingested wins. A type appearing in both forms ends up with both fields populated and Source = HandAuthored.
A polyglot example
Section titled “A polyglot example”Suppose a TypeScript service exports a User shape and you want it in the same ontology graph as your .NET trading types. The TypeScript side has no .NET assembly, so reach the graph through an IOntologySource and an OntologyDelta.AddObjectType:
using System.Runtime.CompilerServices;using Strategos.Ontology;using Strategos.Ontology.Descriptors;
public sealed class TypeScriptUserSource : IOntologySource{ public string SourceId => "scip-typescript:identity-service";
public async IAsyncEnumerable<OntologyDelta> LoadAsync( [EnumeratorCancellation] CancellationToken ct) { var descriptor = new ObjectTypeDescriptor { Name = "User", DomainName = "identity", SymbolKey = "scip-typescript npm identity-service 1.4.0 ./src/models/user.ts/User#", LanguageId = "typescript", Source = DescriptorSource.Ingested, SourceId = SourceId, IngestedAt = DateTimeOffset.UtcNow, };
yield return new OntologyDelta.AddObjectType(descriptor) { SourceId = SourceId, Timestamp = DateTimeOffset.UtcNow, };
await Task.CompletedTask; }
public async IAsyncEnumerable<OntologyDelta> SubscribeAsync( [EnumeratorCancellation] CancellationToken ct) { await Task.CompletedTask; yield break; }}Register the source alongside your domains:
services.AddOntology(options =>{ options.AddDomain<TradingOntology>(); options.AddSource<TypeScriptUserSource>();});OntologyGraphBuilder drains every registered source’s LoadAsync after hand-authored domains compile, so ingested deltas can reference existing hand descriptors. The User descriptor lands in the composed graph with Source = Ingested and LanguageId = "typescript".
AONT037: catching missing identity at build time
Section titled “AONT037: catching missing identity at build time”The descriptor-by-name overload can be called without supplying either a Type or a symbolKey: argument — illegal at runtime, easy to miss in review.
AONT037 PolyglotInvariantViolated is a Roslyn analyzer that scans DomainOntology.Define bodies for the descriptor-by-name overload and reports an Error when none of these are present: a symbolKey: named argument, a clrType: named argument, or a positional typeof(...) argument.
// AONT037 fires — no identity suppliedbuilder.ObjectType("Foo", domainName: "trading");
// Clean — symbolKey suppliedbuilder.ObjectType("Foo", symbolKey: "scip-typescript ./mod#User", domainName: "trading");
// Clean — typeof() positionalbuilder.ObjectType("Foo", typeof(TradeOrder), "trading");
// Clean — generic overload carries the type parameterbuilder.ObjectType<TradeOrder>();The diagnostic stops the build before a descriptor that would throw at composition time can ship. If you want a SymbolKey-only descriptor, the analyzer message names both fix options.
Where to go next
Section titled “Where to go next”- Getting Started — the hand-authored DSL.
- Similarity Search — works against polyglot and CLR descriptors alike.