diff --git a/src/MeshWeaver.Documentation/Data/Architecture/PortalHeapIsHubs.md b/src/MeshWeaver.Documentation/Data/Architecture/PortalHeapIsHubs.md index 6776d8337a..071cb40e42 100644 --- a/src/MeshWeaver.Documentation/Data/Architecture/PortalHeapIsHubs.md +++ b/src/MeshWeaver.Documentation/Data/Architecture/PortalHeapIsHubs.md @@ -132,21 +132,36 @@ five starting hubs and no disposed hubs, so this specimen does not reproduce the retention above. It does reproduce metadata multiplication: 3,662,876 type definitions describe 378 CLR types, with 3,652,612 separate deferred XML-description graphs. -`TypeDefinition` shares the type's display declaration, culture-independent wordified fallback -name and deferred XML description through a `ConditionalWeakTable`. The weak key permits -collectible types to leave with their assemblies. XML lookup stays on first description access -and still goes through `XmlDocs.Summary`; constructing a definition must not parse XML docs. - -Resource-backed display names and groups are resolved for each definition, rather than caching -their translated values. Key-function builders, collection names, owning addresses and explicit -description overrides remain definition-specific. Icon construction also remains independent. -The registry and Autofac scopes retain their existing ownership boundaries. - -A warmed construction probe retaining 1,000 definitions of `System.String` allocated -1,032,000 bytes before this change and 400,000 afterward, with description graphs decreasing -from 1,000 to one. This is constructor-allocation evidence, not a measured production heap -reduction or a closure of the queue-starvation/OOM investigation. The regression budget and -resource/override/key-isolation tests live in `TypeDefinitionMetadataTest`. +**Why every hub had its own graph.** Every hub builds its own `TypeRegistry` +(`MessageHubConfiguration` chains it to the parent's, up to the mesh-level registry `MeshBuilder` +registers), and every `WithType` / `GetOrAddType` constructed a fresh `TypeDefinition` that +re-derived, from the CLR type alone, its display declaration, wordified name, icon and a deferred +XML-description closure. Nothing about that part depends on the hub — yet it was rebuilt once per +hub per type. On top of that every registry re-seeded the 27 basic types (`String`, `Int32`, +`Address`, `DeliveryFailure`, …) into its own map although its parent already resolved them. + +**What is shared, and where it lives.** The hub-independent part is a `TypeDisplayMetadata` (display +declaration, fallback name, icon, lazy XML summary). The store of them, `TypeDisplayMetadataCache`, is an +INSTANCE created by the ROOT registry of a chain — the mesh-level one — and handed down to every +child registry through the parent chain, so there is one description per CLR type per MESH. It is +never `static` ([No Static State](../NoStaticState)): two independent meshes do not share +descriptions, and nothing outlives the mesh. The table is a `ConditionalWeakTable` so a +collectible NodeType's types still leave with their load context. A registry whose parent is a +`TypeRegistry` no longer seeds the basic types; it resolves them through the parent. + +**What stays per hub.** Each hub still owns its `TypeDefinition` for every type it registers: +collection name, key-function builder, owning address and explicit description overrides are +per-registry registration state and are untouched. Resource-backed display names and groups are +resolved per definition, never cached as translated values. XML lookup stays on first description +access; constructing a definition must not parse XML docs. + +`TypeDisplayMetadataSharedAcrossHubsTest` pins it: 32 hosted hubs registering the same type own 32 +definitions over ONE description (a per-registry store measured 32); two independent meshes must NOT +share one (a process-wide static store fails that negative control); key functions and collection +names stay per registry; a hub-level registry resolves `String` to the mesh registry's own +definition. Resource/override/key-isolation tests for standalone definitions live in +`TypeDefinitionMetadataTest`. This is a measured reduction of per-hub metadata, not a closure of +the queue-starvation/OOM investigation (#5555, #1186). ## Why the LIVE ones are never released diff --git a/src/MeshWeaver.Messaging.Hub/Serialization/TypeDefinition.cs b/src/MeshWeaver.Messaging.Hub/Serialization/TypeDefinition.cs index 6f141689ef..1ea9ce6575 100644 --- a/src/MeshWeaver.Messaging.Hub/Serialization/TypeDefinition.cs +++ b/src/MeshWeaver.Messaging.Hub/Serialization/TypeDefinition.cs @@ -1,9 +1,5 @@ using System.ComponentModel.DataAnnotations; -using System.Reflection; -using System.Runtime.CompilerServices; using MeshWeaver.Domain; -using MeshWeaver.Utils; -using Namotion.Reflection; namespace MeshWeaver.Messaging.Serialization; @@ -14,17 +10,6 @@ namespace MeshWeaver.Messaging.Serialization; /// public record TypeDefinition : ITypeDefinition { - // Hubs register the same framework types independently. Share only metadata belonging to the - // CLR type; the weak key must allow runtime-compiled collectible types to leave with their ALC. - private static readonly ConditionalWeakTable Metadata = new(); - - private sealed class DisplayMetadata(Type type) - { - internal DisplayAttribute? Display { get; } = type.GetCustomAttribute(); - internal string FallbackName { get; } = type.Name.Wordify(); - internal Lazy Description { get; } = new(() => XmlDocs.Summary(type)); - } - /// /// Initializes a type definition, deriving display name, group, order, icon and description from the /// type's , icon attribute and XML doc summary. @@ -32,21 +17,36 @@ private sealed class DisplayMetadata(Type type) /// The CLR type being described. /// The collection / serialization name for the type. /// Builder that resolves the key function for instances of the type. + /// + /// A definition built through this constructor owns a PRIVATE . + /// Registries use the internal overload instead, which takes the mesh's shared description. + /// public TypeDefinition(Type elementType, string typeName, KeyFunctionBuilder keyFunctionBuilder) + : this(elementType, typeName, keyFunctionBuilder, new TypeDisplayMetadata(elementType)) + { + } + + /// + /// Initializes a type definition over an already-built — normally mesh-shared — + /// (see ). + /// + /// The CLR type being described. + /// The collection / serialization name for the type. + /// Builder that resolves the key function for instances of the type. + /// The description of . + internal TypeDefinition(Type elementType, string typeName, KeyFunctionBuilder keyFunctionBuilder, TypeDisplayMetadata displayMetadata) { Type = elementType; CollectionName = typeName; + DisplayMetadata = displayMetadata; - var metadata = Metadata.GetValue(elementType, static type => new DisplayMetadata(type)); - // GetName/GetGroupName resolve ResourceType properties at use time. Cache the declaration, - // never their translated values, so two viewers can still receive different display text. - DisplayName = metadata.Display?.GetName() ?? metadata.FallbackName; + // GetName/GetGroupName resolve ResourceType properties at use time. The description shares + // the declaration, never its translated values, so two viewers can still receive different text. + DisplayName = displayMetadata.Display?.GetName() ?? displayMetadata.FallbackName; - GroupName = metadata.Display?.GetGroupName(); - Order = metadata.Display?.GetOrder(); - var iconAttribute = Type.GetCustomAttribute(); - if (iconAttribute != null) - Icon = new Icon(iconAttribute.Provider, iconAttribute.Id); + GroupName = displayMetadata.Display?.GetGroupName(); + Order = displayMetadata.Display?.GetOrder(); + Icon = displayMetadata.Icon; Key = new(() => keyFunctionBuilder.GetKeyFunction(Type)!); @@ -66,7 +66,7 @@ public TypeDefinition(Type elementType, string typeName, KeyFunctionBuilder keyF // what actually closes the window. // // Description is pure display metadata with a single consumer, so deferring costs nothing. - description = metadata.Description; + description = displayMetadata.Description; } /// @@ -85,6 +85,12 @@ public TypeDefinition(Type elementType, string typeName, KeyFunctionBuilder keyF /// The CLR type being described. public Type Type { get; } + /// + /// The hub-independent description of this definition reads its display + /// metadata from. Every registry of one mesh hands out the SAME instance per CLR type, while the + /// definition itself — collection name, key function, owning address — stays per registry. + /// + public TypeDisplayMetadata DisplayMetadata { get; } /// The human-readable display name, from or the wordified type name. public string DisplayName { get; } /// The collection / serialization name used to identify this type in the mesh. diff --git a/src/MeshWeaver.Messaging.Hub/Serialization/TypeDisplayMetadata.cs b/src/MeshWeaver.Messaging.Hub/Serialization/TypeDisplayMetadata.cs new file mode 100644 index 0000000000..121c630caf --- /dev/null +++ b/src/MeshWeaver.Messaging.Hub/Serialization/TypeDisplayMetadata.cs @@ -0,0 +1,74 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Runtime.CompilerServices; +using MeshWeaver.Domain; +using MeshWeaver.Utils; +using Namotion.Reflection; + +namespace MeshWeaver.Messaging.Serialization; + +/// +/// The immutable, hub-independent part of a : what the CLR type itself +/// declares about how it is displayed — its , the wordified fallback +/// name, its icon, and its (deferred) XML-doc description. +/// +/// One instance per CLR type per MESH. Every hub owns its own +/// (collection names, key functions and owning addresses are per-hub registration state), but what +/// the type declares is the same for all of them, so a hub-level registry reads its description from +/// the of the mesh-level registry it chains to rather than +/// rebuilding it — a heap census of a retained-hub fixture counted 3,662,876 definitions for 378 CLR +/// types, each with its own description graph (see Doc/Architecture/PortalHeapIsHubs). +/// +public sealed class TypeDisplayMetadata +{ + internal TypeDisplayMetadata(Type type) + { + Display = type.GetCustomAttribute(); + FallbackName = type.Name.Wordify(); + var iconAttribute = type.GetCustomAttribute(); + if (iconAttribute != null) + Icon = new Icon(iconAttribute.Provider, iconAttribute.Id); + // 🚨 LAZY, and it must stay lazy — see the note in TypeDefinition's constructor + // (FutuRe.Test teardown SIGSEGV): the XML-docs read must never run on the hub-construction path. + Description = new(() => XmlDocs.Summary(type)); + } + + /// + /// The type's , if declared. The DECLARATION is shared, never the + /// values it resolves: GetName/GetGroupName read ResourceType properties at + /// use time, so each definition resolves them itself. + /// + internal DisplayAttribute? Display { get; } + + /// The wordified type name, used when no name is declared. + internal string FallbackName { get; } + + /// The icon declared by the type's icon attribute, or null. + internal Icon? Icon { get; } + + /// The type's XML-doc summary, resolved on FIRST READ only. + internal Lazy Description { get; } +} + +/// +/// The per-mesh store of s, keyed by CLR type. +/// +/// 🚨 An INSTANCE owned by the mesh-level (itself a mesh-scoped +/// singleton registered by MeshBuilder) and handed down the registry parent chain to every +/// hub-level registry — never a static table, which would outlive the mesh and bleed across +/// meshes and tests (Doc/Architecture/NoStaticState). +/// +/// A , not a dictionary: a runtime-compiled +/// NodeType's types live in a COLLECTIBLE load context, and a strong key would root that context +/// for the lifetime of the mesh. The weak key lets the description leave with its assembly. +/// +internal sealed class TypeDisplayMetadataCache +{ + private readonly ConditionalWeakTable descriptions = new(); + + /// Returns the one description of in this mesh, creating it on first use. + /// The CLR type being described. + /// The shared description. + public TypeDisplayMetadata For(Type type) + => descriptions.GetValue(type, static t => new TypeDisplayMetadata(t)); +} diff --git a/src/MeshWeaver.Messaging.Hub/Serialization/TypeRegistry.cs b/src/MeshWeaver.Messaging.Hub/Serialization/TypeRegistry.cs index 09091c8512..897a9f975f 100644 --- a/src/MeshWeaver.Messaging.Hub/Serialization/TypeRegistry.cs +++ b/src/MeshWeaver.Messaging.Hub/Serialization/TypeRegistry.cs @@ -6,8 +6,20 @@ namespace MeshWeaver.Messaging.Serialization; -internal class TypeRegistry(ITypeRegistry? parent) : ITypeRegistry +internal class TypeRegistry : ITypeRegistry { + private readonly ITypeRegistry? parent; + + public TypeRegistry(ITypeRegistry? parent) + { + this.parent = parent; + descriptions = (parent as TypeRegistry)?.descriptions ?? new TypeDisplayMetadataCache(); + var seeded = parent is TypeRegistry ? [] : BasicTypes; + typeByName = new(seeded.Select(t => + new KeyValuePair(t.Name, new TypeDefinition(t, t.Name, keyFunctionBuilder, descriptions.For(t))))); + nameByType = new(seeded.Select(t => new KeyValuePair(t, t.Name))); + } + private static readonly Type[] BasicTypes = [ typeof(string), @@ -65,10 +77,20 @@ private IEnumerable> LiveDemotedDefinition yield return new KeyValuePair(name, definition); } - private readonly ConcurrentDictionary typeByName = - new(BasicTypes.Select(t => new KeyValuePair(t.Name, new TypeDefinition(t, t.Name, null!)))); - private readonly ConcurrentDictionary nameByType = - new(BasicTypes.Select(t => new KeyValuePair(t, t.Name))); + // ── Sharing with the mesh ─────────────────────────────────────────────────────────────── + // Every hub owns a registry that chains to its parent's, up to the mesh-level registry that + // MeshBuilder registers as a singleton. What a CLR type DECLARES about itself (display attribute, + // wordified name, icon, XML description) is the same for all of them, so the description store + // is created ONCE by the root of the chain and handed down: one TypeDisplayMetadata per CLR type per + // mesh, never one per hub (3,652,612 description graphs for 378 types in the retained-hub heap + // census — Doc/Architecture/PortalHeapIsHubs). An instance, never static (NoStaticState). + private readonly TypeDisplayMetadataCache descriptions; + + // The basic types are seeded ONLY at the root of a chain. A registry whose parent is a + // TypeRegistry resolves them through that parent (every lookup below falls through to it), so + // re-seeding 27 definitions into every hub's own map duplicated them once per hub. + private readonly ConcurrentDictionary typeByName; + private readonly ConcurrentDictionary nameByType; // Resolution-only aliases (full namespace-qualified names) → definition. Consulted by TryGetType so // a full-name $type discriminator still resolves on the way IN, but deliberately NOT part of the // canonical `typeByName` map that the polymorphic resolver enumerates (PolymorphicTypeInfoResolver @@ -132,7 +154,7 @@ internal void CopyOwnRegistrationsTo(ITypeRegistry target) public ITypeRegistry WithType(Type type, string typeName) { typeName ??= type.FullName!; - var typeDefinition = new TypeDefinition(type, typeName, keyFunctionBuilder); + var typeDefinition = new TypeDefinition(type, typeName, keyFunctionBuilder, descriptions.For(type)); // A type whose context has already BEGUN unloading registers into the weak shadow: // its Unloading event will never fire again, so a strong entry could never be // evicted — a permanent root on a superseded assembly. @@ -376,7 +398,7 @@ public bool TryGetType(string name, out ITypeDefinition? typeDefinition) if (TryGetType(underlyingName, out var underlyingDef) && underlyingDef != null) { var nullableType = typeof(Nullable<>).MakeGenericType(underlyingDef.Type); - typeDefinition = new TypeDefinition(nullableType, name, keyFunctionBuilder); + typeDefinition = new TypeDefinition(nullableType, name, keyFunctionBuilder, descriptions.For(nullableType)); return true; } return false; @@ -435,7 +457,7 @@ public bool TryGetType(string name, out ITypeDefinition? typeDefinition) typeDefinition = typeByName[typeName]; return true; } - typeDefinition = new TypeDefinition(type, FormatType(type), keyFunctionBuilder); + typeDefinition = new TypeDefinition(type, FormatType(type), keyFunctionBuilder, descriptions.For(type)); return true; } return parent?.TryGetType(name, out typeDefinition) @@ -503,7 +525,7 @@ public string GetOrAddType(Type type, string? defaultName = null) return parentTypeName; typeName = defaultName ?? FormatType(type); - var definition = new TypeDefinition(type, typeName, keyFunctionBuilder); + var definition = new TypeDefinition(type, typeName, keyFunctionBuilder, descriptions.For(type)); // A late registration from an already-unloading context goes to the weak shadow — // a strong entry could never be evicted again (see WithType). if (IsUnloadingContext(type)) @@ -537,7 +559,7 @@ public ITypeRegistry WithKeyFunctionProvider(Func key) if (create) { typeName ??= FormatType(type); - var created = new TypeDefinition(type, typeName, keyFunctionBuilder); + var created = new TypeDefinition(type, typeName, keyFunctionBuilder, descriptions.For(type)); // A late registration from an already-unloading context goes to the weak shadow — // a strong entry could never be evicted again (see WithType). if (IsUnloadingContext(type)) diff --git a/test/MeshWeaver.Messaging.Hub.Test/TypeDefinitionMetadataTest.cs b/test/MeshWeaver.Messaging.Hub.Test/TypeDefinitionMetadataTest.cs index 68746fcae8..9b85e2d69f 100644 --- a/test/MeshWeaver.Messaging.Hub.Test/TypeDefinitionMetadataTest.cs +++ b/test/MeshWeaver.Messaging.Hub.Test/TypeDefinitionMetadataTest.cs @@ -8,25 +8,6 @@ namespace MeshWeaver.Messaging.Hub.Test; public class TypeDefinitionMetadataTest { - [Fact] - public void RepeatedFrameworkDefinitions_HaveBoundedConstructionAllocation() - { - var builder = new KeyFunctionBuilder(); - for (var i = 0; i < 32; i++) - _ = new TypeDefinition(typeof(string), "String", builder); - - var held = new TypeDefinition[1000]; - var before = GC.GetAllocatedBytesForCurrentThread(); - for (var i = 0; i < held.Length; i++) - held[i] = new TypeDefinition(typeof(string), "String", builder); - var allocated = GC.GetAllocatedBytesForCurrentThread() - before; - - // The original per-definition reflection/Wordify/lazy-description graph costs 1,032,000 - // bytes for this workload. Keep a fixed budget that rejects that measured regression. - allocated.Should().BeLessThan(600_000); - GC.KeepAlive(held); - } - [Fact] public void CachedDeclaration_StillResolvesResourcePropertiesForEveryDefinition() { diff --git a/test/MeshWeaver.Messaging.Hub.Test/TypeDisplayMetadataSharedAcrossHubsTest.cs b/test/MeshWeaver.Messaging.Hub.Test/TypeDisplayMetadataSharedAcrossHubsTest.cs new file mode 100644 index 0000000000..78ad5b8611 --- /dev/null +++ b/test/MeshWeaver.Messaging.Hub.Test/TypeDisplayMetadataSharedAcrossHubsTest.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using MeshWeaver.Domain; +using MeshWeaver.Fixture; +using MeshWeaver.Messaging.Serialization; +using Xunit; + +namespace MeshWeaver.Messaging.Hub.Test; + +/// +/// Pins the sharing of immutable type descriptions (#5555, #1186): every hub owns its own +/// and its own s — collection name, key +/// function and owning address are per-hub registration state — but what a CLR type DECLARES about +/// itself is described ONCE per mesh. A retained-hub heap census counted 3,662,876 definitions for +/// 378 CLR types with 3,652,612 separate description graphs: one per hub per type. +/// +/// The negative controls are what make the positive ones mean something: two INDEPENDENT +/// meshes must NOT share a description (a process-wide static table would pass the positive test +/// and fail this one — the sharing must be mesh-scoped), and a definition built outside any registry +/// owns a private one. +/// +public class TypeDisplayMetadataSharedAcrossHubsTest(ITestOutputHelper output) : HubTestBase(output) +{ + private const int HubCount = 32; + + /// A type with a display declaration and an icon, so its description is non-trivial. + [Display(Name = "Described content", GroupName = "Sharing")] + [Icon("test", "described")] + public record DescribedContent(string Label); + + [Fact] + public void HubsOfOneMesh_RegisterTheSameType_ShareOneDescription() + { + var definitions = Enumerable.Range(0, HubCount) + .Select(i => Mesh.GetHostedHub(new Address("described", i.ToString()), + c => c.WithType(typeof(DescribedContent), nameof(DescribedContent)) + .WithPostingIdentity(PostingIdentity.System))) + .Select(hub => hub.TypeRegistry.GetTypeDefinition(typeof(DescribedContent), create: false)) + .Select(d => d.Should().BeOfType().Subject) + .ToArray(); + + // Per-hub registration semantics are unchanged: every hub owns its own definition … + definitions.Distinct(ReferenceEqualityComparer.Instance).Should().HaveCount(HubCount, + "each hub registered the type itself and owns that registration"); + // … and all of them read ONE description. + definitions.Select(d => d.DisplayMetadata).Distinct(ReferenceEqualityComparer.Instance) + .Should().ContainSingle("the description of a CLR type is immutable and mesh-scoped"); + definitions.Should().AllSatisfy(d => + { + d.DisplayName.Should().Be("Described content"); + d.GroupName.Should().Be("Sharing"); + d.Icon.Should().Be(new Icon("test", "described")); + }); + + // The mesh hub's own registry — the root of the chain — hands out the same instance. + var meshDefinition = Mesh.TypeRegistry.GetTypeDefinition(typeof(DescribedContent)) + .Should().BeOfType().Subject; + meshDefinition.DisplayMetadata.Should().BeSameAs(definitions[0].DisplayMetadata); + } + + [Fact] + public void IndependentMeshes_DoNotShareDescriptions() + { + // Negative control: the sharing is scoped to ONE mesh. A static table would make these equal. + var meshA = MessageHubExtensions.CreateTypeRegistry(); + var meshB = MessageHubExtensions.CreateTypeRegistry(); + var a1 = Describe(MessageHubExtensions.CreateTypeRegistry(meshA).WithType(typeof(DescribedContent), "A1")); + var a2 = Describe(MessageHubExtensions.CreateTypeRegistry(meshA).WithType(typeof(DescribedContent), "A2")); + var b1 = Describe(MessageHubExtensions.CreateTypeRegistry(meshB).WithType(typeof(DescribedContent), "B1")); + + a1.Should().NotBeSameAs(a2, "each hub-level registry owns its own definition"); + a1.DisplayMetadata.Should().BeSameAs(a2.DisplayMetadata, "both registries chain to mesh A"); + b1.DisplayMetadata.Should().NotBeSameAs(a1.DisplayMetadata, "mesh B is a different mesh"); + + // A definition constructed outside any registry owns a private description. + new TypeDefinition(typeof(DescribedContent), "Standalone", new KeyFunctionBuilder()) + .DisplayMetadata.Should().NotBeSameAs(a1.DisplayMetadata); + } + + [Fact] + public void SharedDescription_KeepsKeyFunctionsAndNamesPerRegistry() + { + var mesh = MessageHubExtensions.CreateTypeRegistry(); + var first = MessageHubExtensions.CreateTypeRegistry(mesh) + .WithKeyFunctionProvider(t => t == typeof(DescribedContent) ? new KeyFunction(_ => "first", typeof(string)) : null) + .WithType(typeof(DescribedContent), "FirstName"); + var second = MessageHubExtensions.CreateTypeRegistry(mesh) + .WithKeyFunctionProvider(t => t == typeof(DescribedContent) ? new KeyFunction(_ => "second", typeof(string)) : null) + .WithType(typeof(DescribedContent), "SecondName"); + var firstDefinition = Describe(first); + var secondDefinition = Describe(second); + var content = new DescribedContent("x"); + + firstDefinition.DisplayMetadata.Should().BeSameAs(secondDefinition.DisplayMetadata); + firstDefinition.CollectionName.Should().Be("FirstName"); + secondDefinition.CollectionName.Should().Be("SecondName"); + firstDefinition.GetKey(content).Should().Be("first"); + secondDefinition.GetKey(content).Should().Be("second"); + } + + [Fact] + public void BasicTypes_AreSeededOnlyAtTheRootOfTheChain() + { + var mesh = MessageHubExtensions.CreateTypeRegistry(); + var hubRegistries = Enumerable.Range(0, HubCount) + .Select(_ => MessageHubExtensions.CreateTypeRegistry(mesh)) + .ToArray(); + var meshString = mesh.GetTypeDefinition(typeof(string), create: false); + mesh.TryGetCollectionName(typeof(int?), out var meshNullableName).Should().BeTrue(); + + meshString.Should().NotBeNull(); + hubRegistries.Should().AllSatisfy(r => + { + r.GetTypeDefinition(typeof(string), create: false).Should().BeSameAs(meshString, + "a hub-level registry resolves the basic types through the mesh registry"); + r.TryGetCollectionName(typeof(int?), out var name).Should().BeTrue(); + name.Should().Be(meshNullableName); + r.GetType("Int32").Should().Be(typeof(int)); + }); + + // Negative control: an independent root seeds its OWN basic types. + MessageHubExtensions.CreateTypeRegistry().GetTypeDefinition(typeof(string), create: false) + .Should().NotBeNull().And.NotBeSameAs(meshString); + } + + private static TypeDefinition Describe(ITypeRegistry registry) + => registry.GetTypeDefinition(typeof(DescribedContent), create: false) + .Should().BeOfType().Subject; +}