Skip to main content

Validation Pattern

Overview

Validation is the most common use of PreCreate, PreUpdate, and PromoteActionCommand programs. The pattern checks attribute values against business rules and either blocks the operation by returning an error result (or throwing an exception), or allows it to proceed.

When to Use

  • Enforce mandatory fields that cannot be configured declaratively (e.g., fields required only under certain conditions).
  • Cross-attribute or cross-instance validations (e.g., start date must be before end date; related instance must be in a specific state).
  • Uniqueness checks that go beyond platform-level name uniqueness.
  • State-transition guards in PromoteActionCommand (e.g., cannot approve unless all checklist items are complete).

Applicable Program Types

Program TypeInterfaceExecution
PreCreate TriggerIPreCreateTriggerProgramAsyncSynchronous (in transaction)
PreUpdate TriggerIPreUpdateTriggerProgramAsyncSynchronous (in transaction)
Promote Action CommandIPromoteActionCommandProgramAsyncSynchronous (in transaction)

Pattern Structure

  1. Read attribute values from the incoming Instance using typed accessor methods (GetTextAttributeValue, GetDateAttributeValue, etc.).
  2. Apply business rule checks.
  3. Return ProgramResult { IsSuccess = false, Errors = [...] } (or throw an Exception) to block the operation.
  4. Return ProgramResult { IsSuccess = true } to allow the operation.

Note: Throwing an Exception is equivalent to returning IsSuccess = false and is a common alternative in older code. Prefer returning ProgramResult with meaningful Errors for better user-facing messages.

Code Example

This example validates dates, a required user assignment, and a unique external reference ID — all within a single PreCreate program.

using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Builders;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Services;
using Prorigo.Protrak.Programs;
using System;
using System.Threading.Tasks;

namespace YourNamespace.Programs
{
/// <summary>
/// Type: Pre Create Trigger
/// Configured for: Type Project
/// Trigger: Before project instance creation
/// Summary: Validates mandatory dates, owner assignment, and unique external reference ID.
/// </summary>
public class ValidationOnProjectCreate : IPreCreateTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }

private readonly string TYPE_PROJECT = "Project";
private readonly string ATTRIBUTE_START_DATE = "ScheduledStartDate";
private readonly string ATTRIBUTE_END_DATE = "ScheduledEndDate";
private readonly string ATTRIBUTE_OWNER = "OwnerUser";
private readonly string ATTRIBUTE_EXTERNAL_ID = "ExternalReferenceId";

public async Task<ProgramResult> RunAsync(Instance instance)
{
// 1. Date range validation
var startDate = instance.GetDateAttributeValue(ATTRIBUTE_START_DATE);
var endDate = instance.GetDateAttributeValue(ATTRIBUTE_END_DATE);

if (startDate == null)
throw new Exception("Please select a Scheduled Start Date.");
if (endDate == null)
throw new Exception("Please select a Scheduled End Date.");
if (startDate > endDate)
throw new Exception("Scheduled End Date must be after Scheduled Start Date.");

// 2. Required user attribute validation
var owner = instance.GetUserAttributeValue(ATTRIBUTE_OWNER);
if (owner == null || owner.Length == 0)
throw new Exception("Please assign an Owner.");

// 3. Uniqueness check — query for an existing instance with the same external ID
var externalId = instance.GetTextAttributeValue(ATTRIBUTE_EXTERNAL_ID);
if (!string.IsNullOrEmpty(externalId))
{
var existing = await InstanceService.GetInstancesAsync(
InstanceQueryBuilder.ForType(TYPE_PROJECT)
.Where(ATTRIBUTE_EXTERNAL_ID, AttributeFilterOperator.Equals, externalId)
.Take(1)
.Build());
if (existing != null && existing.TotalCount > 0)
throw new Exception($"A project already exists with External Reference ID '{externalId}'.");
}

return new ProgramResult { IsSuccess = true };
}
}
}

Validate on State Transition (PromoteActionCommand)

public class ValidateBeforeApproval : IPromoteActionCommandProgramAsync
{
public async Task<ProgramResult> RunAsync(Instance instance, string fromState, string toState, string actionName)
{
if (toState == "Approved")
{
var comment = instance.GetTextAttributeValue("ApprovalComment");
if (string.IsNullOrWhiteSpace(comment))
{
return new ProgramResult
{
IsSuccess = false,
Errors = new[] { "Approval comment is required before approving." }
};
}
}
return new ProgramResult { IsSuccess = true };
}
}

Key Services

ServiceMethodPurpose
IInstanceServiceGetInstanceAsyncFetch the current persisted value of the instance to compare against the incoming update
IInstanceServiceGetInstancesAsyncQuery for existing instances to enforce uniqueness
IInstanceServiceGetRelatedInstancesAsyncQuery related instances to validate cross-type business rules

Common Gotchas

  • Not all attributes are present. The incoming instance.Attributes only contains attributes submitted by the caller. Use typed accessors (GetDateAttributeValue, etc.) — they return null safely when an attribute is missing, so you only need to null-check the returned value, not an intermediate Attribute object.
  • instance.Attributes in PreUpdate is not the full instance. It only contains the attributes being updated. Use InstanceService.GetInstanceAsync(instance.Id, attributes) to fetch the current persisted values for comparison.
  • Do not perform irreversible side effects (e.g., sending emails) inside a Pre-trigger. If the operation is rolled back, the side effect cannot be undone.