Conga Product Documentation

Welcome to the new doc site. Some of your old bookmarks will no longer work. Please use the search bar to find your desired topic.

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

RuleDescriptionExample
Refactor legacy codeUpdate existing methods to accept a CancellationTokenpublic async Task OnDataChangeAsync(DataChangeEvent dataChangeEvent, CancellationToken cancellationToken)
First line = cancellation checkImmediately call ThrowIfCancellationRequested() at method or loop entryThrowIfCancellationRequested(cancellationToken);
Loop safetyCall ThrowIfCancellationRequested(cancellationToken); at the start of each iterationInside foreach, for, while, etc.
Token is mandatoryAll methods must include a CancellationToken parameterasync Task<string> GetCustomcodesAsync(CancellationToken cancellationToken)
Propagate the tokenPass CancellationToken to all nested or child method callsawait GetCustomcodesAsync(cancellationToken);
Mark non-compliant codeMark legacy methods without tokens as [Obsolete][Obsolete("Use version with CancellationToken")]
No suppression allowedThrowIfCancellationRequested() must not be wrapped in try-catchtry { 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.

Note: Use latest template 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();
        }
    }
}
Custom API
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.

When to Use What
ScenarioCancellation Handling Approach
Class inherits from CodeExtensibility

Use the built-in method: cancellationToken.ThrowIfCancellationRequested();

Class inherits from a base class

(for example, BaseObject or any other custom base class)

Use the helper method: new CancellationTokenHelper().ThrowIfCancellationRequested(cancellationToken);

Note: In both cases, always place the cancellation check at the start of each method and at the beginning of every loop iteration to ensure proper cancellation handling.
Scenario 1: Class Inheriting from CodeExtensibility (using built-in cancellation)
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>>();
    }
}
Scenario 2: Class inherits from a base class (using 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; }
}
Scenario 1: Class Inheriting from CodeExtensibility (using built-in cancellation)
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>>();
    }
}