Phase 2 WP-1–13+23: Template Engine CRUD, composition, overrides, locking, collision detection, acyclicity
- WP-23: ITemplateEngineRepository full EF Core implementation - WP-1: Template CRUD with deletion constraints (instances, children, compositions) - WP-2–4: Attribute, alarm, script definitions with lock flags and override granularity - WP-5: Shared script CRUD with syntax validation - WP-6–7: Composition with recursive nesting and canonical naming - WP-8–11: Override granularity, locking rules, inheritance/composition scope - WP-12: Naming collision detection on canonical names (recursive) - WP-13: Graph acyclicity (inheritance + composition cycles) Core services: TemplateService, SharedScriptService, TemplateResolver, LockEnforcer, CollisionDetector, CycleDetector. 358 tests pass.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using ScadaLink.Commons.Entities.Templates;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
using ScadaLink.Commons.Types;
|
||||
|
||||
namespace ScadaLink.TemplateEngine.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Enforces template deletion constraints (WP-25).
|
||||
/// 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;
|
||||
|
||||
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>
|
||||
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.");
|
||||
|
||||
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
|
||||
var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
|
||||
var childTemplates = allTemplates.Where(t => t.ParentTemplateId == templateId).ToList();
|
||||
if (childTemplates.Count > 0)
|
||||
{
|
||||
var names = string.Join(", ", childTemplates.Select(t => t.Name).Take(10));
|
||||
errors.Add($"Cannot delete template '{template.Name}': {childTemplates.Count} child template(s) inherit from it ({names}{(childTemplates.Count > 10 ? "..." : "")}).");
|
||||
}
|
||||
|
||||
// Check 3: Other templates compose it
|
||||
var composingTemplates = new List<(string TemplateName, string InstanceName)>();
|
||||
foreach (var t in allTemplates)
|
||||
{
|
||||
var compositions = await _repository.GetCompositionsByTemplateIdAsync(t.Id, cancellationToken);
|
||||
foreach (var comp in compositions)
|
||||
{
|
||||
if (comp.ComposedTemplateId == templateId)
|
||||
composingTemplates.Add((t.Name, comp.InstanceName));
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user