c7774b7ec2
InstanceService.SetAlarmOverrideAsync and TemplateDeletionService move to the no-tracking, body-free GetAllTemplatesForAnalysisAsync; TemplateFolderService counts folder contents from GetTemplateStubsAsync.
125 lines
6.4 KiB
C#
125 lines
6.4 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
|
|
|
|
/// <summary>
|
|
/// Enforces template deletion constraints.
|
|
/// Template deletion is blocked when:
|
|
/// - Instances reference the template
|
|
/// - Child templates reference it (as parent)
|
|
/// - Other templates compose it
|
|
/// Returns clear error messages listing the referencing entities.
|
|
/// </summary>
|
|
public class TemplateDeletionService
|
|
{
|
|
private readonly ITemplateEngineRepository _repository;
|
|
|
|
/// <summary>
|
|
/// Initializes a new <see cref="TemplateDeletionService"/> with the given repository.
|
|
/// </summary>
|
|
/// <param name="repository">Repository used to check deletion constraints and perform the delete.</param>
|
|
public TemplateDeletionService(ITemplateEngineRepository repository)
|
|
{
|
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether a template can be safely deleted and returns any blocking reasons.
|
|
/// </summary>
|
|
/// <param name="templateId">The id of the template to check.</param>
|
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
/// <returns>A task that resolves to a successful result if the template can be deleted, or a failure with blocking reasons.</returns>
|
|
public async Task<Result<bool>> CanDeleteTemplateAsync(int templateId, CancellationToken cancellationToken = default)
|
|
{
|
|
var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
|
|
if (template == null)
|
|
return Result<bool>.Failure($"Template with ID {templateId} not found.");
|
|
|
|
if (template.IsDerived)
|
|
return Result<bool>.Failure(
|
|
$"Cannot delete template '{template.Name}': it is a derived template. " +
|
|
"Remove the owning composition on its parent template instead.");
|
|
|
|
var errors = new List<string>();
|
|
|
|
// Check 1: Instances reference this template
|
|
var instances = await _repository.GetInstancesByTemplateIdAsync(templateId, cancellationToken);
|
|
if (instances.Count > 0)
|
|
{
|
|
var names = string.Join(", ", instances.Select(i => i.UniqueName).Take(10));
|
|
errors.Add($"Cannot delete template '{template.Name}': {instances.Count} instance(s) reference it ({names}{(instances.Count > 10 ? "..." : "")}).");
|
|
}
|
|
|
|
// Check 2: Child templates reference it as parent. Split derived vs. regular.
|
|
// Reads ids, names, parent/derived/owner edges and composition rows only — the
|
|
// no-tracking, script-body-free analysis projection carries all of them
|
|
// (review 2026-10-21 04/P6, 08/P1).
|
|
var allTemplates = await _repository.GetAllTemplatesForAnalysisAsync(cancellationToken);
|
|
var inheritors = allTemplates.Where(t => t.ParentTemplateId == templateId).ToList();
|
|
var regularChildren = inheritors.Where(t => !t.IsDerived).ToList();
|
|
var derivatives = inheritors.Where(t => t.IsDerived).ToList();
|
|
|
|
if (regularChildren.Count > 0)
|
|
{
|
|
var names = string.Join(", ", regularChildren.Select(t => t.Name).Take(10));
|
|
errors.Add($"Cannot delete template '{template.Name}': {regularChildren.Count} child template(s) inherit from it ({names}{(regularChildren.Count > 10 ? "..." : "")}).");
|
|
}
|
|
|
|
if (derivatives.Count > 0)
|
|
{
|
|
var compIds = derivatives.Select(d => d.OwnerCompositionId).Where(id => id.HasValue).Select(id => id!.Value).ToHashSet();
|
|
var ownerLookup = allTemplates
|
|
.SelectMany(t => t.Compositions.Select(c => new { Owner = t, Composition = c }))
|
|
.Where(x => compIds.Contains(x.Composition.Id))
|
|
.ToDictionary(x => x.Composition.Id, x => $"'{x.Owner.Name}' (as '{x.Composition.InstanceName}')");
|
|
var details = string.Join(", ", derivatives.Take(10).Select(d =>
|
|
d.OwnerCompositionId.HasValue && ownerLookup.TryGetValue(d.OwnerCompositionId.Value, out var label)
|
|
? label
|
|
: $"'{d.Name}'"));
|
|
errors.Add($"Cannot delete template '{template.Name}': it is the base of {derivatives.Count} derived template(s) used in {details}{(derivatives.Count > 10 ? "..." : "")}. Remove those compositions first.");
|
|
}
|
|
|
|
// Check 3: Other templates compose it directly.
|
|
// Read the Compositions navigation already loaded by GetAllTemplatesForAnalysisAsync
|
|
// rather than issuing one GetCompositionsByTemplateIdAsync round-trip per
|
|
// template — this is the same source TemplateService
|
|
// .DeleteTemplateAsync uses for the equivalent check.
|
|
var composingTemplates = allTemplates
|
|
.SelectMany(t => t.Compositions
|
|
.Where(comp => comp.ComposedTemplateId == templateId)
|
|
.Select(comp => (TemplateName: t.Name, comp.InstanceName)))
|
|
.ToList();
|
|
|
|
if (composingTemplates.Count > 0)
|
|
{
|
|
var details = string.Join(", ",
|
|
composingTemplates.Take(10).Select(c => $"'{c.TemplateName}' (as '{c.InstanceName}')"));
|
|
errors.Add($"Cannot delete template '{template.Name}': {composingTemplates.Count} template(s) compose it ({details}{(composingTemplates.Count > 10 ? "..." : "")}).");
|
|
}
|
|
|
|
if (errors.Count > 0)
|
|
return Result<bool>.Failure(string.Join(" ", errors));
|
|
|
|
return Result<bool>.Success(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a template after checking all constraints.
|
|
/// </summary>
|
|
/// <param name="templateId">The id of the template to delete.</param>
|
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
/// <returns>A task that resolves to a successful result when deleted, or a failure with the blocking reason.</returns>
|
|
public async Task<Result<bool>> DeleteTemplateAsync(int templateId, CancellationToken cancellationToken = default)
|
|
{
|
|
var canDelete = await CanDeleteTemplateAsync(templateId, cancellationToken);
|
|
if (canDelete.IsFailure)
|
|
return canDelete;
|
|
|
|
await _repository.DeleteTemplateAsync(templateId, cancellationToken);
|
|
await _repository.SaveChangesAsync(cancellationToken);
|
|
return Result<bool>.Success(true);
|
|
}
|
|
}
|