move to Convention based templates - #74
Merged
Merged
Conversation
…ates Breaking change simplifying the whole registration surface: - TemplateStore is keyed by model type. Named registration and the name-based Render overloads are gone; Render<TModel> is the whole render surface, and a template registered against an abstract base accepts subclass instances. - ParchmentModelAttribute is parameterless. The template is found by convention: the AdditionalFiles entry named after the type (TypeName.docx or TypeName.md). None is PARCH004, more than one is PARCH020. - Style documents move to .dotx, markdown flow only: TypeName.dotx wins (two of them is the new PARCH021), else the nearest parchment.dotx up the directory tree, else the built-in blank. - The generator embeds templates into the generated source (docx/dotx as base64, markdown as a string literal) and registers them from a module initializer. RegisterWith, TemplatePath/TemplateName, basePath, CopyToOutputDirectory and the ParchmentEmbeddedTemplate staging are gone; a TemplateStore materializes the stored definition on the model's first render, under its own image policies. - SG snapshots scrub the embedded base64 (OPC packages are not byte-reproducible); the byte-level assertions live in TemplateConventionTests. The old paragraph-level docx caching test is inverted: byte changes must re-emit or the embedded copy goes stale.
TryResolveMember(type, name, out memberType) with [NotNullWhen(true)], so callers branch on the bool instead of null-checking, and the path walks in ReferenceValidator / MarkdownReferenceValidator collapse the scope lookup and the member resolve into one guard.
ShapeResolver.ResolveMember becomes TryResolveMember with a [NotNullWhen(true)] out MemberEntry, matching the runtime ModelValidator.TryResolveMember shape; the three generator call sites fold the null check into the guard. ModelSymbolResolver.ResolveMember had no callers left and is deleted.
ModelValidator.TryResolveElementType and ModelSymbolResolver.TryGetElementType carried the Try prefix but returned nullable; both now use bool + [NotNullWhen(true)] out, matching EditableMap.TryGetElementType, and the callers fold the null checks into their guards.
TryResolveRoot(modelType, name, scope, out rootType) with [NotNullWhen(true)], so Validate throws straight from the guard.
TryResolvePathType with [NotNullWhen(true)] in ReferenceValidator and MarkdownReferenceValidator; the loop-source callers fold the null check into their guards.
Resolve becomes TryResolve and GetElementType becomes TryGetElementType, both with [NotNullWhen(true)] out string FQNs. The generator and MarkdownValidator call sites fold their null checks into guards — the loop-source resolve-then-element chain in each validator collapses to one condition — and ShapeResolverTests assert the bool.
TryResolveElementType(source, loopVariable, out elementType): false is the untyped-source case (range, assign target) where the loop variable is accepted unchecked; a resolvable-but-not-enumerable source still throws. VisitForStatement branches on the result, typed case first.
The "One template per model" section demonstrated manual registration as if it were the primary flow, when the recommended path has no registration call at all. It now opens with the [ParchmentModel] + convention + render shape and demotes RegisterMarkdownTemplate to what it is — the hatch for runtime-produced templates and non-partial models. nuget-readme reordered the same way: generator first, registering by hand second.
The source generator is now the only path for model metadata. A model bound to a compile-time template carries [ParchmentModel]; a model registered by hand against a runtime-supplied template carries the new [ParchmentBindable], whose module initializer registers the same pre-compiled accessors and per-template maps without embedding a template. SharedFluid.EnsureModelRegistered rejects a model the generator never saw; the maps resolve precompiled-or-empty. Deleted outright: SharedFluid.RegisterTypeGraph, the WalkType reflection walks in ExcelsiorTableMap / FormatMap / StringListMap / EditableMap (setter building, NullabilityInfoContext, collection construction), ModelGraph, and StaticRenderAttributes with its logger warnings. Registration-time token validation stays reflective on purpose - a runtime template still has to be checked against the model. Shape rules the deleted walks enforced move to compile time: - PARCH022: editable collection element must be constructable, carry an editable member, and not nest a further editable collection. - PARCH023: conflicting [Html]/[Markdown]/[StringSyntax] markers. Their runtime negative tests are deleted; SG tests cover both, plus the bindable emission, ParchmentModel-wins, and inheritance-chain cases. Migration surfaced three latent SG bugs, fixed here: array types walked System.Array members into illegal accessors, StringBuilder.AppendJoin under the netstandard2.0/Polyfill build stringified the path list, and the repeating-section element factory used `new T()`, which cannot compile for elements with required members (now Activator, matching the old runtime factory). KeyValuePair<K,V> is special-cased in the shape so dictionary iteration binds on the SG path; the root accessor block is always emitted so even a member-less model registers as SG-known. Test models across Parchment.Tests, Morph.Tests and IntegrationTests now carry [ParchmentBindable] with the generator wired into each project; obsolete runtime-only negatives (StaticRenderAttributeTests, shape-error registration throws) are removed in favour of the compile- time diagnostics.
An empty editable map has two causes and only one of them was named: a model the generator never saw now gets the "no pre-compiled Parchment accessors" message (via SharedFluid.IsModelRegistered / the shared NotGeneratedMessage) instead of the misleading "declares no [EditableField] members" that a genuinely field-less model earns. The extractor also drops its private per-type map cache — EditableMap.Build is a single dictionary lookup now, so the second layer was pure overhead. Tests cover both messages.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Parchment 5.0.0
Templates are now found by convention, embedded into your assembly at compile time, and rendered by model type. There is no registration call, no template file to deploy, and no template name to repeat. Model binding is entirely source-generated — the runtime reflection layer is gone.
This is a breaking release. Every consumer needs the migration steps below.
Templates are found by convention
[ParchmentModel]takes no arguments. The template is theAdditionalFilesentry named after the model type —Invoice.docxorInvoice.md— wherever it sits in the project.No match is
PARCH004; more than one (a.docxand a.md, or namesakes in different folders) isPARCH020. Both are reported rather than guessed at.Templates are embedded; nothing to register, nothing to deploy
The generator embeds the template into the generated source and registers it from a
[ModuleInitializer]when your assembly loads. ATemplateStorematerializes it on the model's first render.The generated
RegisterWith,TemplatePathandTemplateNamemembers are removed, as is the<ParchmentEmbeddedTemplate>MSBuild item and its manifest-resource staging — every template is embedded now.<ParchmentTemplate>remains, and no longer setsCopyToOutputDirectory.Rendering is keyed by model type
All name-based overloads are removed. A model has exactly one template; re-registering replaces it.
If you relied on several templates per model, give each its own model type — a thin wrapper class holding the shared data is enough. Registering against an abstract base still accepts subclass instances at render.
Style documents move to
.dotxand are found by conventionA markdown template's style source is resolved in order:
TypeName.dotx, else the nearestparchment.dotxwalking up the directory tree from the template, else the built-in blank. Declare them asAdditionalFileslike templates; the resolved file is embedded alongside. TwoTypeName.dotxmatches is the newPARCH021. Docx templates are unaffected — they carry their own styles.No runtime reflection over models
Every binding model now goes through the generator, which emits its member accessors and per-template maps at compile time. There is no reflection fallback: a model the generator never saw is rejected at registration (and at extraction) with a message naming the fix.
Hand registration is still supported for templates the generator cannot see — content produced at runtime, per-tenant templates. The model just has to be marked:
A model that cannot carry the attribute (a third-party or generated type) is no longer registrable — wrap it in a thin
partialmodel class.Trimming and NativeAOT: with the reflection walks gone, model binding is fully static.
New diagnostics
PARCH021PARCH022[EditableField]collection element isn't constructable, has no editable members, or nests another editable collectionPARCH023[Html]/[Markdown]/[StringSyntax]markers on one memberPARCH022andPARCH023were previously runtime registration exceptions; they now fail the build.PARCH019(render attribute on a static member) is compile-time only — the correspondingILoggerwarning at registration is removed.Fixed
Four model-binding bugs surfaced while making the generator the only path — all of them affected
[ParchmentModel]users already:System.Array's own members, producing source that could not compile.StringBuilder.AppendJoinbinding to the params-object overload).new T(), which cannot compile for element types withrequiredmembers.IDictionary<K,V>members did not bind on the generated path;KeyValuePairis now part of the emitted shape.Also
ParchmentExtractor.Extract<T>distinguishes "model was never source-generated" from "model declares no[EditableField]members" instead of reporting the latter for both.Resolve*helpers across the runtime and generator moved to theTry*+[NotNullWhen(true)]pattern.