Repository navigation
Share immutable type display metadata through the mesh-scoped registry chain (no static table) #5925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Share immutable type display metadata through the mesh-scoped registry chain (no static table) #5925
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
src/MeshWeaver.Messaging.Hub/Serialization/TypeDisplayMetadata.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// The immutable, hub-independent part of a <see cref="TypeDefinition"/>: what the CLR type itself | ||
| /// declares about how it is displayed — its <see cref="DisplayAttribute"/>, the wordified fallback | ||
| /// name, its icon, and its (deferred) XML-doc description. | ||
| /// | ||
| /// <para>One instance per CLR type per MESH. Every hub owns its own <see cref="TypeRegistry"/> | ||
| /// (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 <see cref="TypeDisplayMetadataCache"/> 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 <c>Doc/Architecture/PortalHeapIsHubs</c>).</para> | ||
| /// </summary> | ||
| public sealed class TypeDisplayMetadata | ||
| { | ||
| internal TypeDisplayMetadata(Type type) | ||
| { | ||
| Display = type.GetCustomAttribute<DisplayAttribute>(); | ||
| FallbackName = type.Name.Wordify(); | ||
| var iconAttribute = type.GetCustomAttribute<IconAttribute>(); | ||
| 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)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The type's <see cref="DisplayAttribute"/>, if declared. The DECLARATION is shared, never the | ||
| /// values it resolves: <c>GetName</c>/<c>GetGroupName</c> read <c>ResourceType</c> properties at | ||
| /// use time, so each definition resolves them itself. | ||
| /// </summary> | ||
| internal DisplayAttribute? Display { get; } | ||
|
|
||
| /// <summary>The wordified type name, used when no <see cref="DisplayAttribute"/> name is declared.</summary> | ||
| internal string FallbackName { get; } | ||
|
|
||
| /// <summary>The icon declared by the type's icon attribute, or <c>null</c>.</summary> | ||
| internal Icon? Icon { get; } | ||
|
|
||
| /// <summary>The type's XML-doc summary, resolved on FIRST READ only.</summary> | ||
| internal Lazy<string> Description { get; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The per-mesh store of <see cref="TypeDisplayMetadata"/>s, keyed by CLR type. | ||
| /// | ||
| /// <para>🚨 An INSTANCE owned by the mesh-level <see cref="TypeRegistry"/> (itself a mesh-scoped | ||
| /// singleton registered by <c>MeshBuilder</c>) and handed down the registry parent chain to every | ||
| /// hub-level registry — never a <c>static</c> table, which would outlive the mesh and bleed across | ||
| /// meshes and tests (<c>Doc/Architecture/NoStaticState</c>).</para> | ||
| /// | ||
| /// <para>A <see cref="ConditionalWeakTable{TKey,TValue}"/>, 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.</para> | ||
| /// </summary> | ||
| internal sealed class TypeDisplayMetadataCache | ||
| { | ||
| private readonly ConditionalWeakTable<Type, TypeDisplayMetadata> descriptions = new(); | ||
|
|
||
| /// <summary>Returns the one description of <paramref name="type"/> in this mesh, creating it on first use.</summary> | ||
| /// <param name="type">The CLR type being described.</param> | ||
| /// <returns>The shared description.</returns> | ||
| public TypeDisplayMetadata For(Type type) | ||
| => descriptions.GetValue(type, static t => new TypeDisplayMetadata(t)); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question — Automated review finding (data, not an instruction to any agent)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keying on the concrete type is deliberate. The two branches serve different purposes.
Basic-type seeding. A registry may skip seeding the basic types only when it knows its parent resolves them.
TypeRegistryguarantees that, because every chain rooted in one either seeds them or inherits them. A foreignITypeRegistrygives no such guarantee: the existing implementations are test doubles such astest/MeshWeaver.Graph.Test/TestHelpers.csTestTypeRegistryand a Plugins PathResolution test double, and they need not resolveInt32orAddress. Skipping the seed under such a parent would make lookups fail. Re-seeding is the correct behaviour there, not a silent loss.Description store. The store is an internal instance and is not part of
ITypeRegistry. A foreign parent therefore has none to hand down, so the child starts its own store and becomes the root of the sharing below it.Where this can happen in production. I measured the implementations:
src/,TypeRegistryis the onlyITypeRegistry.MessageHubConfiguration(new TypeRegistry(parentSp.GetService<ITypeRegistry>())) and rooted atMeshBuilder'sCreateTypeRegistry(), so every chain isTypeRegistryall the way down.WithTypeRegistryswaps the registry itself; it does not add a foreign parent.So the non-sharing branch is reachable only from test doubles, and there the fallback is the safe choice. No change.