Skip to content

Get template content from the template service, and fix the compiled-views fallback - #1059

Merged
KevinJump merged 1 commit into
v17/mainfrom
v17/fix-template-content-fallback
Sep 1, 2026
Merged

Get template content from the template service, and fix the compiled-views fallback#1059
KevinJump merged 1 commit into
v17/mainfrom
v17/fix-template-content-fallback

Conversation

@KevinJump

Copy link
Copy Markdown
Owner

What

When importing a template, we now ask Umbraco's ITemplateService for the file content first — the way Umbraco itself does it — before falling back to the view filesystem, and finally to a placeholder when the views are compiled into the site.

Getting that fallback chain to actually work needed two fixes:

GetFileContentStreamAsync never returns null. TemplateRepository.GetFileContentStream hands back Stream.Null when the file is missing (the signature is Task<Stream>, not Task<Stream?>). Checking for null meant we always took the "found it" branch, read an empty string, and created the template with no content. The view filesystem fallback and the compiled-views placeholder below it were unreachable, and a genuinely missing template file was silently imported as an empty template rather than failing with the "local file is missing" warning.

The placeholder wasn't parseable. Umbraco derives the master template by running TemplateContentParserService over the content, and its regex — \s*Layout\s*=\s*"?(?<layout>[\w\s\.]*)"?; — requires the trailing semi-colon. { Layout = "master" } never matched, so the parent was never set on a template created this way. It's now written as valid razor:

@{
    Layout = "master.cshtml";
}
<!-- [uSyncMarker:...] template content - will be removed -->

Root templates get Layout = null; instead of pointing Umbraco at a template called "".

Also aligns ViewPath (and the equivalent in TemplateHandler) with how Umbraco names the view file — the alias verbatim, see TemplateRepository.SetVirtualPath — rather than stripping spaces, so we don't look for, or delete, the wrong file for an alias containing a space.

Tidy-ups in the same files

  • Removed the CleanseNode override. It removed a "Content" element, but the element is "Contents" — it has never done anything in any shipped version. Rather than "fix" the name, it's deleted: TemplateTracker explicitly tracks /Contents, so making the strip work would have the change hash ignore a change the tracker reports. Behaviour is unchanged; the dead code is gone.
  • Dropped the unused _shortStringHelper field. The constructor parameter stays so the signature isn't broken.
  • Flattened ShouldGetContentFromNode — it had null checks on non-nullable injected services. All four input combinations give the same answers as before.
  • GetContentFromFile is now private (nothing in OpenSource or Products calls it) with tidier stream disposal.
  • Moved the content fetch inside the create branch — the update path was paying for a filesystem read it discarded, and could fail an import over a file it didn't need.
  • Removed a stray using Lucene.Net.Queries.Function.ValueSources;.

Tests

Three new tests in TemplateSerializerTests, covering the no-file-on-disk path: creating a child template whose parent must be resolved from the placeholder, the root-template case, and the views-not-compiled case that should fail rather than silently create an empty template.

The parent test asserts the generated content parses back to "master" using Umbraco's real TemplateContentParserService, rather than re-implementing the regex — that parse is the actual mechanism by which the master gets set.

Both bugs above are covered: reverting the fixes fails these tests with Expected: "master" But was: null / content <string.Empty>, and Expected: False But was: True with message "Created".

Full suite: 144 passed, 0 failed.

Note for reviewers

BuildFileSystems in the test fixture now sets up IIOHelper.PathStartsWith. These are the first tests to reach _viewFileSystem.FileExists, and PhysicalFileSystem.GetFullPath uses that call to check a path is inside its root — unmocked it returns false, so every path looked out-of-root and FileExists threw UnauthorizedAccessException instead of returning false. Test-harness gap, not a product issue, but worth knowing for future tests that touch the view filesystem.

TemplateSerializer.cs exists in v18/main with the same code, so this needs forward-porting.

🤖 Generated with Claude Code

…views fallback

When importing a template we now ask Umbraco's ITemplateService for the file
content first (the way Umbraco does it), before falling back to the view
filesystem, and finally to a placeholder when the views are compiled.

Two things had to be fixed for that fallback to actually work:

* TemplateService.GetFileContentStreamAsync never returns null - the repository
  hands back Stream.Null when the file is missing. Checking for null meant we
  always took the "found it" branch, read an empty string, and created the
  template with no content - so the view filesystem fallback and the
  compiled-views placeholder below it were unreachable, and a genuinely missing
  template file was silently imported as an empty one instead of failing.

* The placeholder we hand Umbraco is parsed by TemplateContentParserService to
  work out the master template, and its regex requires a trailing semi-colon.
  "{ Layout = "master" }" never matched, so the parent was never set. It is now
  written as valid razor - @{ Layout = "master.cshtml"; } - and root templates
  get Layout = null; rather than an empty alias.

Also aligns ViewPath (and the handler's equivalent) with how Umbraco names the
view file - the alias verbatim, see TemplateRepository.SetVirtualPath - rather
than stripping spaces, so we don't look for or delete the wrong file.

Tidy up while in here:
 * remove the CleanseNode override - it looked for a "Content" element, but the
   element is "Contents", so it has never done anything. TemplateTracker
   explicitly tracks /Contents, so stripping it now would make the hash and the
   tracker disagree - behaviour is unchanged, the dead code is gone.
 * drop the unused _shortStringHelper field (constructor signature kept)
 * flatten the dead null checks in ShouldGetContentFromNode
 * make GetContentFromFile private and tidy its stream disposal
 * remove a stray Lucene using

Tests: covers creating a child template when there is no file on disk, asserting
the placeholder parses back to the parent alias using Umbraco's own parser, plus
the root-template and views-not-compiled cases. BuildFileSystems now sets up
IIOHelper.PathStartsWith, without which PhysicalFileSystem throws instead of
reporting a missing file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KevinJump
KevinJump merged commit bf6b356 into v17/main Sep 1, 2026
5 checks passed
@KevinJump
KevinJump deleted the v17/fix-template-content-fallback branch September 1, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v17: uSync Template Import Fails with “Failed to create template” on Umbraco 17.5.3

1 participant