Skip to content

Fix: Cannot create templates during sync when running in production mode (Umbraco 17.3+)#938

Merged
KevinJump merged 5 commits intov17/mainfrom
v17/production-templates
Apr 8, 2026
Merged

Fix: Cannot create templates during sync when running in production mode (Umbraco 17.3+)#938
KevinJump merged 5 commits intov17/mainfrom
v17/production-templates

Conversation

@KevinJump
Copy link
Copy Markdown
Owner

@KevinJump KevinJump commented Apr 8, 2026

Addresses all outstanding review comments on the production-mode template creation workaround introduced in the previous commit.

SyncTemplateService

  • CreateAsync: Repository fallback now only triggers when already in production mode or when the service returns NotAllowedInProductionMode. Previously it fell through to the repo on any service failure, bypassing service-level validation and eventing for unrelated errors (e.g. duplicate alias).
// Before: fell back to repo on ANY service failure
if (IsInProductionMode() is false)
{
    var attempt = await _templateService.CreateAsync(...);
    if (attempt.Success)
        return attempt;
    // fell through to repo even for DuplicateAlias, InvalidAlias, etc.
}

// After: only falls back for production-mode-specific rejection
if (IsInProductionMode() is false)
{
    var attempt = await _templateService.CreateAsync(...);
    if (attempt.Success) return attempt;
    if (attempt.Status is not TemplateOperationStatus.NotAllowedInProductionMode)
        return attempt;  // honour the service's rejection
}
  • CreateTemplateInternal: Removed spurious async (no awaits present); returns Task.FromResult(...) directly. Template construction moved before the try so template is non-null in the catch. Removed redundant scope.Complete()autoComplete: true with using var is sufficient. Failure status in catch is now NotAllowedInProductionMode instead of the misleading Success.

  • XML docs: Fixed "espeically" typo, corrected class summary, replaced wrong <param name="description"> with accurate param tags for all five parameters.

TemplateSerializer

  • Removed unused IShortStringHelper shortStringHelper constructor parameter and its using directive.
  • Removed unreachable second if (item is null) block — the preceding creation branch always exits via return on both success and failure paths.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a template-service wrapper to work around Umbraco 17.3.0 limitations when creating templates in Production runtime mode, and wires uSync’s template import/handler paths through that wrapper so template imports can succeed in production deployments.

Changes:

  • Add ISyncTemplateService / SyncTemplateService that delegates to ITemplateService when possible and falls back to repository-based creation in production mode.
  • Register the new wrapper service in AdduSyncCore.
  • Update serializers/handlers to depend on ISyncTemplateService (and improve template creation failure logging in TemplateSerializer).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
uSync.Core/uSyncCoreBuilderExtensions.cs Registers ISyncTemplateService in DI so core/backoffice components can use the wrapper.
uSync.Core/Templates/ISyncTemplateService.cs New abstraction for the subset of template operations uSync needs.
uSync.Core/Templates/SyncTemplateService.cs Implements production-mode workaround by creating templates via repository + scope.
uSync.Core/Serialization/Serializers/TemplateSerializer.cs Uses ISyncTemplateService for template creation and adds more detailed failure logging.
uSync.Core/Serialization/Serializers/ContentTemplateSerializer.cs Switches dependency from ITemplateService to ISyncTemplateService.
uSync.Core/Serialization/Serializers/ContentSerializer.cs Switches dependency from ITemplateService to ISyncTemplateService for template lookups during content import.
uSync.BackOffice/SyncHandlers/Handlers/TemplateHandler.cs Switches handler dependency to ISyncTemplateService for template tree operations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +122 to +126
if (item is null)
{
// creating went wrong
logger.LogWarning("Failed to create template");
return SyncAttempt<ITemplate>.Fail(name, ChangeType.Import, "Failed to create template");
logger.LogWarning("Failed to create template - item is null after create process.");
return SyncAttempt<ITemplate>.Fail(name, ChangeType.Import, "Failed to create template - no new item created.");
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if (item is null) block after the creation branch is effectively unreachable because the item is null case above returns on both success and failure paths. Keeping this block makes control flow confusing; consider removing it (or restructuring the earlier branch) so there’s a single, reachable null-handling path.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the unreachable if (item is null) block in cb1c7ca. The first if (item is null) block always exits via return on both the failure and success paths, so the second check was dead code.

Comment on lines +43 to +56
public async Task<Attempt<ITemplate, TemplateOperationStatus>> CreateAsync(string name, string alias, string? content, Guid userKey, Guid key)
{
TemplateOperationStatus lastKnownStatus = TemplateOperationStatus.Success;

if (IsInProductionMode() is false)
{
var attempt = await _templateService.CreateAsync(name, alias, content, userKey, key);
if (attempt.Success)
return attempt;
}

// else - lets try the repository way (surely this is a hack?)
// https://github.com/umbraco/Umbraco-CMS/pull/21600#issuecomment-4205232583
return await CreateTemplateInternal(name, alias, content, userKey, key, lastKnownStatus);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastKnownStatus is initialized to TemplateOperationStatus.Success and never updated when _templateService.CreateAsync(...) fails. If the repository save then throws, this method will return a failed Attempt with a Success status, which is misleading for callers and logs. Capture the failed attempt’s status (and potentially return the original attempt when not in production) so failure status accurately reflects what happened.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 025f2de. CreateAsync now captures the failed attempt and returns it directly unless the status is NotAllowedInProductionMode — in that case it falls through to the repository path. The CreateTemplateInternal catch now also uses TemplateOperationStatus.NotAllowedInProductionMode as the failure status instead of Success.

Comment on lines +47 to +56
if (IsInProductionMode() is false)
{
var attempt = await _templateService.CreateAsync(name, alias, content, userKey, key);
if (attempt.Success)
return attempt;
}

// else - lets try the repository way (surely this is a hack?)
// https://github.com/umbraco/Umbraco-CMS/pull/21600#issuecomment-4205232583
return await CreateTemplateInternal(name, alias, content, userKey, key, lastKnownStatus);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateAsync falls back to repository creation even when not in production mode if _templateService.CreateAsync(...) fails for any reason (e.g., validation/duplicate alias). That bypasses service-level validation/eventing and can create templates in cases the service correctly rejected. Consider only using the repository workaround when in production mode (or when the failure status is the specific “not allowed in production” case), otherwise return the original failed attempt.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 025f2de. CreateAsync now only falls back to the repository workaround when: (1) already in production mode, or (2) not in production mode but the service returned NotAllowedInProductionMode. Any other service failure (validation, duplicate alias, etc.) is returned directly to the caller.

Comment on lines +59 to +79
private async Task<Attempt<ITemplate, TemplateOperationStatus>> CreateTemplateInternal(string name, string alias, string? content, Guid userKey, Guid key, TemplateOperationStatus lastKnownStatus)
{
var template = new Template(_shortStringHelper, name, alias)
{
Content = content,
Key = key,
};

try
{
using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
{
_templateRepository.Save(template);
scope.Complete();

return Attempt.SucceedWithStatus<ITemplate, TemplateOperationStatus>(TemplateOperationStatus.Success, template);
}
}
catch (Exception ex)
{
return Attempt.FailWithStatus<ITemplate, TemplateOperationStatus>(lastKnownStatus, template, ex);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateTemplateInternal is marked async but contains no await, which adds unnecessary state-machine overhead and can hide synchronous exceptions behind an async boundary. Either make it synchronous and return Task.FromResult(...)/Task directly, or introduce real async work (there currently isn’t any).

Suggested change
private async Task<Attempt<ITemplate, TemplateOperationStatus>> CreateTemplateInternal(string name, string alias, string? content, Guid userKey, Guid key, TemplateOperationStatus lastKnownStatus)
{
var template = new Template(_shortStringHelper, name, alias)
{
Content = content,
Key = key,
};
try
{
using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
{
_templateRepository.Save(template);
scope.Complete();
return Attempt.SucceedWithStatus<ITemplate, TemplateOperationStatus>(TemplateOperationStatus.Success, template);
}
}
catch (Exception ex)
{
return Attempt.FailWithStatus<ITemplate, TemplateOperationStatus>(lastKnownStatus, template, ex);
private Task<Attempt<ITemplate, TemplateOperationStatus>> CreateTemplateInternal(string name, string alias, string? content, Guid userKey, Guid key, TemplateOperationStatus lastKnownStatus)
{
ITemplate? template = null;
try
{
template = new Template(_shortStringHelper, name, alias)
{
Content = content,
Key = key,
};
using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
{
_templateRepository.Save(template);
scope.Complete();
return Task.FromResult(Attempt.SucceedWithStatus<ITemplate, TemplateOperationStatus>(TemplateOperationStatus.Success, template));
}
}
catch (Exception ex)
{
return Task.FromResult(Attempt.FailWithStatus<ITemplate, TemplateOperationStatus>(lastKnownStatus, template, ex));

Copilot uses AI. Check for mistakes.
using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
{
_templateRepository.Save(template);
scope.Complete();
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope usage is inconsistent: CreateCoreScope(autoComplete: true) implies the scope will complete on dispose, but the code also calls scope.Complete(). This is redundant at best and may be confusing for future changes; prefer either autoComplete: false + explicit Complete(), or keep autoComplete: true and remove the explicit Complete() call (matching other uSync code such as SyncDocumentUrlCleaner).

Suggested change
scope.Complete();

Copilot uses AI. Check for mistakes.
Copilot AI changed the title workaround the fact that you can't create templates in production mode via the service. Remove unreachable null check in TemplateSerializer.DeserializeCoreAsync Apr 8, 2026
@KevinJump
Copy link
Copy Markdown
Owner Author

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 8, 2026

@copilot apply changes based on the comments in this thread

Applied all changes in 025f2de:

  • SyncTemplateService.CreateAsync: only falls back to repo in production mode or when NotAllowedInProductionMode status is returned; captures the failed attempt status correctly
  • SyncTemplateService.CreateTemplateInternal: removed async, now returns Task.FromResult(...) directly; moved template construction before the try block; replaced autoComplete: true + scope.Complete() with just autoComplete: true via using var
  • TemplateSerializer: removed unused IShortStringHelper shortStringHelper parameter and its using directive
  • XML docs: fixed typos and corrected <param> tags

@KevinJump KevinJump changed the title Remove unreachable null check in TemplateSerializer.DeserializeCoreAsync Fix : Unable to create templates in Production mode Umbraco 17.3+ Apr 8, 2026
Copilot AI changed the title Fix : Unable to create templates in Production mode Umbraco 17.3+ Clean up SyncTemplateService and TemplateSerializer based on review feedback Apr 8, 2026
@KevinJump KevinJump changed the title Clean up SyncTemplateService and TemplateSerializer based on review feedback Fix: Cannot create templates during sync when running in production mode (Umbraco 17.3+) Apr 8, 2026
@KevinJump KevinJump merged commit 2417448 into v17/main Apr 8, 2026
4 checks passed
@KevinJump KevinJump deleted the v17/production-templates branch April 8, 2026 13:41
KevinJump added a commit that referenced this pull request Apr 9, 2026
…uction mode (Umbraco 17.3+) (#938)"

This reverts commit 2417448.
KevinJump added a commit that referenced this pull request Apr 9, 2026
…uction mode (Umbraco 17.3+) (#938)" (#939)

This reverts commit 2417448.
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.

3 participants