Conversation
…e via the service.
There was a problem hiding this comment.
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/SyncTemplateServicethat delegates toITemplateServicewhen 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 inTemplateSerializer).
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.
| 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."); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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).
| 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)); |
| using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) | ||
| { | ||
| _templateRepository.Save(template); | ||
| scope.Complete(); |
There was a problem hiding this comment.
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).
| scope.Complete(); |
…oreAsync Agent-Logs-Url: https://github.com/KevinJump/uSync/sessions/fb414557-0558-4a8e-95a4-3e1f9151e895 Co-authored-by: KevinJump <431231+KevinJump@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread |
…erializer Agent-Logs-Url: https://github.com/KevinJump/uSync/sessions/b3cad61a-cdc8-44c7-81e5-33f304567a05 Co-authored-by: KevinJump <431231+KevinJump@users.noreply.github.com>
Applied all changes in
|
Agent-Logs-Url: https://github.com/KevinJump/uSync/sessions/b3cad61a-cdc8-44c7-81e5-33f304567a05 Co-authored-by: KevinJump <431231+KevinJump@users.noreply.github.com>
Addresses all outstanding review comments on the production-mode template creation workaround introduced in the previous commit.
SyncTemplateServiceCreateAsync: Repository fallback now only triggers when already in production mode or when the service returnsNotAllowedInProductionMode. Previously it fell through to the repo on any service failure, bypassing service-level validation and eventing for unrelated errors (e.g. duplicate alias).CreateTemplateInternal: Removed spuriousasync(no awaits present); returnsTask.FromResult(...)directly. Template construction moved before thetrysotemplateis non-null in the catch. Removed redundantscope.Complete()—autoComplete: truewithusing varis sufficient. Failure status in catch is nowNotAllowedInProductionModeinstead of the misleadingSuccess.XML docs: Fixed "espeically" typo, corrected class summary, replaced wrong
<param name="description">with accurate param tags for all five parameters.TemplateSerializerIShortStringHelper shortStringHelperconstructor parameter and itsusingdirective.if (item is null)block — the preceding creation branch always exits viareturnon both success and failure paths.