CancellationToken Implementation Guidelines
This document provides implementation guidelines for using CancellationToken in custom code developed within the Conga Advantage Platform. It explains why cancellation support is important, outlines the coding standards to follow, and introduces enforcement strategies through custom analyzers. You will also find migration steps for legacy code and real-world examples that demonstrate how to apply cancellation patterns in both callback and service hook scenarios.
What is CancellationToken?
CancellationToken is a lightweight struct in .NET used for cooperative cancellation of asynchronous or long-running operations. It allows tasks to gracefully stop execution when a cancellation is requested. Developers must periodically check the token and exit cleanly to ensure stable task termination.
Why is it Important?
With .NET 5.0 and above, the runtime no longer supports forcefully aborting threads or tasks. Instead, it encourages cooperative cancellation to enable:
-
Safe resource management
-
Predictable and graceful shutdowns
-
Easier debugging and maintenance
-
Prevention of memory corruption or inconsistent states
-
Reliable operations in multithreaded and asynchronous environments.
This is especially important in web APIs, background services, and microservices.
How Do We Ensure Compliance in Custom Code?
Cancellation works only if your code explicitly checks for the token. To ensure consistency and error prevention:
-
Design patterns for token usage are mandated.
-
Custom code analyzers are in place to detect violations during compile time and flag them early.
These checks help maintain code quality and operational stability.
Developer Guidelines for Using CancellationToken
| Rule | Description | Example |
|---|---|---|
| Refactor legacy code | Update existing
methods to accept a CancellationToken | public async Task
OnDataChangeAsync(DataChangeEvent dataChangeEvent, CancellationToken
cancellationToken) |
| First line = cancellation check | Immediately call ThrowIfCancellationRequested() at
method or loop entry | ThrowIfCancellationRequested(cancellationToken); |
| Loop safety | Call ThrowIfCancellationRequested(cancellationToken); at the start of each iteration | Inside foreach, for, while,
etc. |
| Token is mandatory | All methods must include a CancellationToken parameter | async
Task<string> GetCustomcodesAsync(CancellationToken cancellationToken) |
| Propagate the token | Pass CancellationToken to all nested or
child method calls | await
GetCustomcodesAsync(cancellationToken); |
| Mark non-compliant code | Mark legacy methods
without tokens as [Obsolete] | [Obsolete("Use
version with CancellationToken")] |
| No suppression allowed | ThrowIfCancellationRequested() must not be wrapped in try-catch | ❌ try
{ ThrowIfCancellationRequested(cancellationToken); } catch {} as it should be first line of code for every method |
Migration Strategy
To ensure token support in existing custom code:
-
Audit methods that do not support
CancellationToken. -
Refactor them to accept and propagate the token.
-
Inherit from
CodeExtensibility(if not already). -
Mark old versions as
[Obsolete]. -
Add
ThrowIfCancellationRequested(cancellationToken);as the first line of every method and loop. -
Educate teams on best practices.
Conga.Platform.Extensibility.Templates-202505.2.50 for implementation examples.Code Examples for Common Use Cases
Callback
using Conga.Revenue.Common.Callback;
using Conga.Revenue.Common.Callback.Models;
using Conga.Platform.Extensibility.CustomCode.Library;
using System.Collections.Generic;
using System.Threading.Tasks;
using Conga.Revenue.Common.Callback.Entities;
using Conga.Revenue.Common.Callback.Messages;
using System.Threading;
using Conga.Platform.Extensibility.CustomCode.Library.Interfaces;
using System;
namespace DemoDisplayActionCallback
{
public class DisplayActionCallback : CodeExtensibility, IDisplayActionCallback
{
[Obsolete]
public Task ExecuteDisplayAction(IActionRequest request)
{
throw new NotImplementedException("Please use the overload with CancellationToken support");
}
public async Task ExecuteDisplayAction(IActionRequest request, CancellationToken cancellationToken)
{
ThrowIfCancellationRequested(cancellationToken);
var actions = request.GetDisplayActionInfo();
var cartContext = request.GetCartContext();
var cartName = cartContext.GetCart().GetEntity().Name;
if (cartName.Contains("DisplayActionCallback01-Auto-CART-WithError"))
{
foreach (var action in actions)
{
ThrowIfCancellationRequested(cancellationToken);
if(action.ActionName == "GoToPricing" || action.ActionName == "Finalize" || action.ActionName == "Abandon")
{
action.IsEnabled = false;
}
}
}
await Task.CompletedTask;
}
}
}
Service Hook
using System.Threading.Tasks;
using Conga.Platform.Extensibility.CustomCode.Library;
using Conga.Platform.Extensibility.CustomCode.Library.Models;
using Conga.Platform.Extensibility.CustomCode.Library.ServiceHooks;
using Conga.Platform.Extensibility.CustomCode.Library.Interfaces;
using Conga.Platform.Extensibility.CustomCode.Library;
using System.Collections.Generic;
using Conga.Platform.Common.Models;
using Conga.Revenue.Common.Callback.Models;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
namespace Conga.Sample
{
public class Account : BaseObject
{
public string BillingCity { get; set; }
public string Description { get; set; }
}
public class EventProcessor : CodeExtensibility, IDataChangeServiceHooks
{
[Obsolete("Use version with CancellationToken")]
public Task OnDataChangeAsync(DataChangeEvent dataChangeEvent)
{
throw new NotImplementedException("Please use the overload with CancellationToken support");
}
public async Task OnDataChangeAsync(DataChangeEvent dataChangeEvent, CancellationToken cancellationToken)
{
ThrowIfCancellationRequested(cancellationToken);
var dbHelper = GetDataHelper();
var logHelper = GetLogHelper();
//loop example starts
int res = 1;
while (res <=20)
{
ThrowIfCancellationRequested(cancellationToken);
//await Task.Delay(1000);
logHelper.LogInformation($"Count is {res}");
res++;
}
//loop example end
await dbHelper.InsertAsync("<object-name>", new Dictionary<string, object>()
{
{ "Name", "Test Name" }
});
await GetCustomcodesAsync(cancellationToken);
}
public async Task<string> GetCustomcodesAsync(CancellationToken cancellationToken)
{
ThrowIfCancellationRequested(cancellationToken);
string requestUri = "api/extensibility/v1/customcode/customaccountcrudnew";
InternalApiAttributes internalApiAttributes = new();
var httpHelper = GetHttpHelper(internalApiAttributes);
var response = await httpHelper.GetAsync(requestUri);
return await response.Content?.ReadAsStringAsync();
}
}
}
using Conga.Platform.Common.Models;
using Conga.Platform.Extensibility.CustomCode.Library;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Conga.Platform
{
public class CustomObject : BaseObject
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class AccountController : CodeExtensibility
{
string objectName = "CustomObject_c";
public async Task<List<Dictionary<string, dynamic>>> Get(string id, CancellationToken cancellationToken)
{
ThrowIfCancellationRequested(cancellationToken);
var dbHelper = base.GetDataHelper();
var query = dbHelper.CreateQuery(objectName);
var newQuery = query.Where($"Id='{id}'");
var response = await dbHelper.QueryAsync(newQuery);
//loop example starts
int res = 1;
while (res <=20)
{
ThrowIfCancellationRequested(cancellationToken);
//await Task.Delay(1000);
logHelper.LogInformation($"Count is {res}");
res++;
}
//loop example end
return response.Result;
}
public async Task<Dictionary<string, object>> Create(CustomObject custObj, CancellationToken cancellationToken)
{
ThrowIfCancellationRequested(cancellationToken);
//Some complex validation logic...
var dbHelper = base.GetDataHelper();
Dictionary<string, object> objectInput = new()
{
{ "Name", custObj.FirstName + custObj.LastName },
{ "FirstName_c", custObj.FirstName },
{ "LastName_c", custObj.LastName },
{ "EmailAddress_c", custObj.Email }
};
return await dbHelper.InsertAsync(objectName, objectInput);
}
public async Task<Dictionary<string, object>> Update(CustomObject custObj, CancellationToken cancellationToken)
{
ThrowIfCancellationRequested(cancellationToken);
//Some complex validation logic...
var dbHelper = base.GetDataHelper();
Dictionary<string, object> objectInput = new()
{
{ "Id", custObj.Id },
{ "FirstName_c", custObj.FirstName },
{ "LastName_c", custObj.LastName },
{ "EmailAddress_c", custObj.Email }
};
return await dbHelper.UpdateAsync(objectName, objectInput);
}
public async Task<bool> Delete(string id, CancellationToken cancellationToken)
{
ThrowIfCancellationRequested(cancellationToken);
var dbHelper = base.GetDataHelper();
return await dbHelper.DeleteAsync(objectName, id);
}
}
}Managing Multi-Inheritance
CodeExtensibility is a framework-provided base class that supports common extensibility needs such as cancellation handling. Normally, classes that inherit CodeExtensibility can use these built-in feature directly.
In some scenarios, developers need to create classes that inherit from a base class (for example, BaseObject) and implement several methods. These methods must support CancellationToken to avoid analyzer errors related to missing cancellation handling.
Since these classes cannot inherit from both BaseObject and CodeExtensibility, the solution is to use CancellationTokenHelper for cancellation checks.
Refer to the 202508.3.72 release custom code templates for the multi-inheritance use case.
| Scenario | Cancellation Handling Approach |
|---|---|
Class inherits from CodeExtensibility | Use the built-in method: |
| Class inherits from a base class (for example, | Use the helper method: |
public class SampleAccount : CodeExtensibility
{
private Dictionary<string, string> _fields = new Dictionary<string, string>();
public async Task<List<Dictionary<string, string>>> TestAsync(int count, CancellationToken cancellationToken)
{
// Built-in cancellation support
ThrowIfCancellationRequested(cancellationToken);
// Start telemetry span for logging
using var span = GetTelemetryHelper().StartActiveSpan($"{nameof(SampleAccount)}.{nameof(this.TestAsync)}");
AttributeMatrixEntry attributeMatrixEntry = new AttributeMatrixEntry();
attributeMatrixEntry.SetValue("Status_c", "Initialized", cancellationToken);
span.AddLog($"Status_c: Initialized");
// Simulated processing
span.AddLog($"Going to set value of Status_c to OUT");
attributeMatrixEntry.SetValue("Status_c", "Out", cancellationToken);
span.AddLog($"Status_c: Out");
return new List<Dictionary<string, string>>();
}
}
CancellationTokenHelper)public class AttributeMatrixEntry : BaseObject
{
public void SetValue(string fieldName, object value, CancellationToken cancellationToken)
{
new CancellationTokenHelper().ThrowIfCancellationRequested(cancellationToken);
for (int i = 0; i < 10; i++)
{
new CancellationTokenHelper().ThrowIfCancellationRequested(cancellationToken);
// Custom logic here
}
base.SetValue(fieldName, value);
}
public object GetValue(AttributeMatrixEntry entry, string fieldName, CancellationToken cancellationToken)
{
new CancellationTokenHelper().ThrowIfCancellationRequested(cancellationToken);
var attributeMatrixEntry = typeof(AttributeMatrixEntry);
for (int i = 0; i < 10; i++)
{
new CancellationTokenHelper().ThrowIfCancellationRequested(cancellationToken);
// Custom logic here
new CancellationTokenHelper().ThrowIfCancellationRequested(cancellationToken);
}
return attributeMatrixEntry.GetProperty(fieldName).GetValue(entry, null);
}
public string Product_ID_c { get; set; }
public string Status_c { get; set; }
public string Customer_Part_c { get; set; }
public string Version_c { get; set; }
public string License_Model_c { get; set; }
public string License_Key_c { get; set; }
public string Order_Type_c { get; set; }
public string Key_Count_c { get; set; }
public string Platform_c { get; set; }
public string Promotion_c { get; set; }
public string Versioned_Part_c { get; set; }
public string Version_Required_c { get; set; }
}
public class SampleAccount : CodeExtensibility
{
private Dictionary<string, string> _fields = new Dictionary<string, string>();
public async Task<List<Dictionary<string, string>>> TestAsync(int count, CancellationToken cancellationToken)
{
// Built-in cancellation support
ThrowIfCancellationRequested(cancellationToken);
// Start telemetry span for logging
using var span = GetTelemetryHelper().StartActiveSpan($"{nameof(SampleAccount)}.{nameof(this.TestAsync)}");
AttributeMatrixEntry attributeMatrixEntry = new AttributeMatrixEntry();
attributeMatrixEntry.SetValue("Status_c", "Initialized", cancellationToken);
span.AddLog($"Status_c: Initialized");
// Simulated processing
span.AddLog($"Going to set value of Status_c to OUT");
attributeMatrixEntry.SetValue("Status_c", "Out", cancellationToken);
span.AddLog($"Status_c: Out");
return new List<Dictionary<string, string>>();
}
}
