Delete Related Data Pattern
Overview
When an instance is deleted, Protrak does not automatically cascade-delete related child instances unless configured to do so. The Delete Related Data pattern uses a PreDelete trigger to find and delete all related child instances before the parent is removed, and optionally disconnect (unlink without deleting) related instances that should be preserved.
When to Use
- Delete all child instances that logically belong to the parent (e.g., delete all tasks, status records, logs, and financial data when a project is deleted).
- Unlink (but not delete) instances that are shared and should survive the parent's deletion.
- Clean up related data to avoid orphaned records.
Applicable Program Types
| Program Type | Interface | Execution |
|---|---|---|
| PreDelete Trigger | IPreDeleteTriggerProgramAsync | Synchronous (in transaction with delete) |
Use PreDelete (not PostDelete). PreDelete runs while the parent instance still exists, so relations can still be queried. PostDelete would run after the parent is gone, making relation queries unreliable.
Pattern Structure
- For each related type that should be deleted: query related instances, loop, and call
InstanceService.DeleteInstance. - For each related type that should only be unlinked: query related instances, loop, and call
RelationService.DeleteRelationusing theRelationIdfrom theRelatedInstance.
Code Example
Before a Project is deleted, all owned child instances are deleted and shared associations are simply unlinked.
using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Contracts.Filters;
using Prorigo.Protrak.API.Services;
using Prorigo.Protrak.Programs;
using System;
using System.Threading.Tasks;
namespace YourNamespace.Programs
{
/// <summary>
/// Type: Pre Delete Trigger
/// Configured for: Type Project
/// Trigger: Before a Project instance is deleted
/// Summary: Deletes all owned child instances (sub-items, tasks, status records, logs,
/// financial records, metrics) and unlinks shared user profiles.
/// </summary>
public class DeleteProjectRelatedData : IPreDeleteTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }
public IRelationService RelationService { get; set; }
// Types to delete (owned by the project)
private readonly string TYPE_SUBITEM = "SubItem";
private readonly string TYPE_FINANCIAL_DATA = "FinancialData";
private readonly string TYPE_METRICS = "Metrics";
private readonly string TYPE_STATUS_RECORD = "StatusRecord";
private readonly string TYPE_LOG = "ActivityLog";
private readonly string TYPE_TASK = "Task";
// Relations for types to delete
private readonly string REL_TO_SUBITEM = "ProjectToSubItem";
private readonly string REL_TO_FINANCIAL = "ProjectToFinancialData";
private readonly string REL_TO_METRICS = "ProjectToMetrics";
private readonly string REL_TO_STATUS_RECORD = "ProjectToStatusRecord";
private readonly string REL_TO_LOG = "ProjectToActivityLog";
private readonly string REL_TO_TASK = "ProjectToTask";
// Types to only unlink (shared — do not delete)
private readonly string TYPE_USER_PROFILE = "UserProfile";
private readonly string REL_TO_USER = "ProjectToUserProfile";
public async Task<ProgramResult> RunAsync(Guid instanceId)
{
// Delete all owned child instances
await DeleteRelatedInstancesAsync(instanceId, TYPE_SUBITEM, REL_TO_SUBITEM);
await DeleteRelatedInstancesAsync(instanceId, TYPE_FINANCIAL_DATA, REL_TO_FINANCIAL);
await DeleteRelatedInstancesAsync(instanceId, TYPE_METRICS, REL_TO_METRICS);
await DeleteRelatedInstancesAsync(instanceId, TYPE_STATUS_RECORD, REL_TO_STATUS_RECORD);
await DeleteRelatedInstancesAsync(instanceId, TYPE_LOG, REL_TO_LOG);
await DeleteRelatedInstancesAsync(instanceId, TYPE_TASK, REL_TO_TASK);
// Only unlink user profiles (shared instances — do not delete them)
await UnlinkRelatedInstancesAsync(instanceId, TYPE_USER_PROFILE, REL_TO_USER);
return new ProgramResult { IsSuccess = true };
}
private async Task DeleteRelatedInstancesAsync(Guid instanceId, string typeName, string relationTypeName)
{
var related = await GetRelatedInstancesAsync(instanceId, typeName, relationTypeName);
if (related?.Items == null) return;
foreach (var item in related.Items)
{
await InstanceService.DeleteInstanceAsync(item.RelatedInstanceId);
}
}
private async Task UnlinkRelatedInstancesAsync(Guid instanceId, string typeName, string relationTypeName)
{
var related = await GetRelatedInstancesAsync(instanceId, typeName, relationTypeName);
if (related?.Items == null) return;
foreach (var item in related.Items)
{
// Delete only the relation, not the related instance
await RelationService.DeleteRelationAsync(item.RelationId);
}
}
private async Task<PagedData<RelatedInstance>> GetRelatedInstancesAsync(
Guid instanceId, string typeName, string relationTypeName)
{
var query = new RelatedInstanceQuery
{
RelationFilters = new[]
{
new RelationFilter
{
TypeName = typeName,
RelationTypeName = relationTypeName,
RelationDirection = RelationDirection.To
}
}
// No Skip/Take needed — fetch all
};
return await InstanceService.GetRelatedInstancesAsync(instanceId, query);
}
}
}
Delete vs. Unlink
| Operation | Service Method | Effect |
|---|---|---|
| Delete | InstanceService.DeleteInstanceAsync(instanceId) | Permanently removes the instance and all its relations |
| Unlink | RelationService.DeleteRelationAsync(relationId) | Removes only the relation; both instances remain |
The RelationId needed for unlink comes from RelatedInstance.RelationId returned by GetRelatedInstancesAsync.
Validation Before Delete (Pre-condition checks)
You can also use a PreDelete trigger to block deletion when business rules are violated:
public async Task<ProgramResult> RunAsync(Guid instanceId)
{
// Check if project has any open tasks before allowing deletion
var openTasks = await GetRelatedInstancesAsync(instanceId, "Task", "ProjectToTask");
if (openTasks != null && openTasks.TotalCount > 0)
{
return new ProgramResult
{
IsSuccess = false,
Errors = new[] { "Cannot delete project with open tasks. Close or delete all tasks first." }
};
}
// No open tasks — allow deletion
return new ProgramResult { IsSuccess = true };
}
Key Services
| Service | Method | Purpose |
|---|---|---|
IInstanceService | DeleteInstanceAsync(instanceId) | Permanently delete an instance |
IInstanceService | GetRelatedInstancesAsync(instanceId, query) | Find related instances to delete/unlink |
IRelationService | DeleteRelationAsync(relationId) | Remove a relation link without deleting instances |