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.

Custom Code Guidelines

This topic outlines standard coding practices to be followed while writing or customizing code in the Conga Advantage Platform. Following these guidelines ensures clean, maintainable, and efficient code.

You can use the custom code analyzer to ensure consistent, efficient, and safe coding practices across all custom code executed via Custom APIs, Callbacks, and Service Hooks.
Note: Contact your Conga Support Representative to enable the custom code analyzer configuration.

Once enabled, this setting enforces custom code analyzer rules at compile time, helping to identify and prevent common coding issues early in the development lifecycle.

The enforced rules include:

  • Cancellation Token Enforcement: Ensures all asynchronous custom code honors CancellationToken parameters. When a cancellation is requested, the system stops the task right away. This helps avoid extra work and makes the application respond faster.

  • Static Reference Restrictions Prevents the use of static variables, methods, and classes in custom code. This avoids memory leaks and unintended behavior due to shared state across executions.

General Guidelines

  • Avoid static variables and methods: Static variables and methods share data across users and requests, which can cause unexpected behavior in multi-tenant environments.

  • Avoid unused code: Remove any unused variables, methods, or imports to keep the codebase clean and readable.

  • Use LINQ instead of foreach loops: Prefer LINQ for cleaner and more expressive data operations when working with collections.

Exception Handling

Proper exception handling is essential for building reliable and stable code. Use the following C# practices:

Key Concepts

  • Try-Catch Block: Wrap error-prone code in a try block and handle exceptions in a catch block.

  • Finally Block: Use finally to execute cleanup code, regardless of whether an exception occurred.

  • Throw Statement: Use throw to re-throw or raise custom exceptions as needed.

Example

public class ExceptionHandlingExample
{
    public static void Main(string[] args)
    {
        try
        {
            int result = Divide(10, 0);
            Console.WriteLine($"Result: {result}");
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        finally
        {
            Console.WriteLine("This is always executed.");
        }
    }

    public static int Divide(int numerator, int denominator)
    {
        return numerator / denominator;
    }
}

Asynchronous Programming

Use async/await correctly
  • Define async methods: Mark a method as async only if it contains at least one await.

  • Always use await: Avoid writing async methods without await—this results in synchronous execution and compiler warnings.

  • Avoid async void: Always return Task or Task<T> from async methods.

public async Task<string> GetDataAsync()
{
    using (var client = new HttpHelper())
    {
        return await client.GetStringAsync("http://example.com/data");
    }
}
Async All the Way
Ensure that the entire call chain is asynchronous. Avoid calling .Result or .Wait(), as they may cause deadlocks or block threads unnecessarily.
public async Task MainAsync()
{
    await PerformOperationAsync();
}

public async Task PerformOperationAsync()
{
    await SomeAsyncOperation();
}

// Calling from a synchronous method
public void Main()
{
    MainAsync().GetAwaiter().GetResult(); // Safe pattern
}
Avoid Fire-and-Forget

Unless writing top-level event handlers, avoid calling async methods without awaiting them.

// Incorrect
public void StartProcessing()
{
    ProcessDataAsync(); // Fire-and-forget
}

// Correct
public async Task StartProcessingAsync()
{
    await ProcessDataAsync();
}

Cancellation Token Usage

Implement CancellationToken in all methods that support cancellation. This allows you to gracefully stop long-running or unnecessary tasks. For more details ,see the Cancellation Token Implementation topic.

Logging Standards

  • Use a standardized approach to log messages.

  • Avoid using JSON serializers for logging unless absolutely necessary.

  • Logging should be controlled via configuration (e.g., enable/disable through a setting).

Custom Code Compilation Checks During Build

To validate custom code during the build process, a post-script runs the Custom Code Compiler Tool. This process helps:

  • Identify cancellation token support, static variable usage, and blocked types.
  • Detect analyzer issues early.
Note: This functionality is available by default in Conga.Platform.Extensibility.Template version 202509.2.* and later. For earlier versions, you can optionally integrate it into your existing post-build script.

Steps to Implement

  1. Locate the Post-Build Script Navigate to the postbuildscript.ps1 file in your project directory.

  2. Update the Script Replace the entire content of postbuildscript.ps1 with the script provided below.
    $ProjectName = $args[0]
    $ProjectDir = $args[1]
    
    [xml]$csprojFile = Get-Content -Path ($ProjectDir + $ProjectName + ".csproj")
    $ReferencedProjects = $csprojFile.SelectNodes('//ProjectReference') | Select Include
    
    # Ensure the custom compiler tool is installed
    $installed = $true
    $toolName = "conga.platform.extensibility.customcode.compiler.tool.nuget"
    $toolList = dotnet tool list --global | Select-String $toolName
    
    if (-not $toolList) {
        Write-Host "Compiler tool not found. Installing $toolName"
        dotnet tool install --global $toolName
        if ($LASTEXITCODE -ne 0) {
            $installed = $false
            Write-Error "Failed to install the compiler tool. Please install the tool manually"
        }
    }
    
    # Run the custom compiler tool only if the function exists
    if ($installed && Get-Command compile-custom-code -ErrorAction SilentlyContinue) {
        $compilerResult = & compile-custom-code $ProjectDir
        if (-not $compilerResult) {
            Write-Error "Custom code compilation checks failed. No output received from the compiler."
            exit 1
        }
        elseif ($compilerResult -match 'error' -or $compilerResult -match 'failed') {
            Write-Error "Custom code compilation checks failed. See output below:"
            $hasErrors = $false
            $projectRoot = Resolve-Path $ProjectDir
            foreach ($line in $compilerResult) {
                $regex = 'Error:\w+\s*\|\s*(?<file>[^()]+\.cs)\((?<line>\d+),(?<col>\d+)\):\s*(?<msg>.*)'
                $match = [regex]::Match($line, $regex)
                if ($match.Success) {
                    $file = Join-Path $projectRoot $match.Groups['file'].Value
                    $lineNum = $match.Groups['line'].Value
                    $colNum = $match.Groups['col'].Value
                    $msg = $match.Groups['msg'].Value
    
                    if (Test-Path $file) {
                        Write-Host "$file($lineNum,$colNum): $msg"
                        $hasErrors = $true
                    } else {
                        Write-Host "$line"
                    }
    
                } else {
                    Write-Host "$line"
                }
            }
            if ($hasErrors) { exit 1 }
            Write-Host "Custom code compilation completed successfully."
        }
        else {
            Write-Host "Custom code compilation completed successfully."
        }
    } else {
        Write-Warning "The 'compile-custom-code' command was not found. Skipping custom code compilation."
    }
    
    $newGuid = New-Guid
    $destinationFolder = 'ZipContent-' + $newGuid
    $excludelist = @('bin', 'obj', $destinationFolder, 'properties', '*.log', '*.csproj', '*.json', '*.exe', '*.dll', '*.pdb', '*.sln', '*.zip', '*.ps1', 'Program.cs')
    
    New-Item -Path $ProjectDir -Name $destinationFolder -ItemType "directory" -Force
    Copy-Item -Path (Get-ChildItem -Path $ProjectDir -Exclude $excludelist) -Destination $ProjectDir$destinationFolder -Recurse -Force
    
    $ReferencedProjects | ForEach-Object {
    	$referencedProjectPath = Split-Path $_.Include
    	Copy-Item -Path (Get-ChildItem -Path $referencedProjectPath -Exclude $excludelist) -Destination $ProjectDir$destinationFolder -Recurse -Force
    }
    
    Compress-Archive -Path (Get-ChildItem -Path $ProjectDir$destinationFolder) -DestinationPath $ProjectDir$ProjectName.zip -Force
    Remove-Item -Path $ProjectDir$destinationFolder -Recurse -Force
    
    
  3. Run the Build Execute the build command and review the Output or Error List console for results.

Debugging Tips

  • The compiler tool runs only if the conga.platform.extensibility.customcode.compiler.tool.nuget package is installed.

  • Check the Output window for installation errors.

  • If the tool is missing, the script will not block the build; it will skip the compilation step and continue.

  • For installation guidance, refer to the link-to-installation-doc.

Callback-Specific Guidelines

For IValidationCallbackBeforePricingValidationAsync

  • You cannot access child objects of LineItem.

  • Use SetToRePricing() to mark a LineItem for repricing.

For IPricingBasePriceCallbackBeforePricingBatchAsync

  • Product information can be accessed directly from LineItemModel.

  • Do not fetch product data from objectDB.

Other Callback Actions

For other callbacks, child objects like Product, PriceList, and PriceListItem should be accessed through LineItemModel, not objectDB.

Handling Lookup Objects

  • LineItem lookup fields on platform entities use different types in pricing entities.

  • Use Dictionary<Key, Value> to get/set these values consistently.

Avoid Self-Instantiation as a Field (Anti-Pattern)

The self-instantiation as field anti-pattern occurs when a class declares a field of its own type and initializes it by creating a new instance of itself. This pattern is incorrect and usually results from misunderstanding how methods can be accessed within the same class.

Incorrect Example
public class MyService
{
    private MyService service = new MyService(); // Anti-pattern

    public void DoWork()
    {
        service.HelperMethod(); // Incorrect method call
    }

    public void HelperMethod()
    {
        // Implementation
    }
}

Why This Is a Problem:

  • Unnecessary object creation: Each instance of the class creates another instance of the same class, which is wasteful in terms of memory and CPU.
  • Risk of infinite or deep instantiation chains: If the constructor or field initialization logic is not carefully controlled, it can lead to recursive construction patterns that can cause stack overflows or excessive resource usage.
  • Incorrect method usage model: It hides the fact that methods like HelperMethod are instance methods that can be called directly (HelperMethod();), which may confuse readers about how to correctly use the API.

Correct Approach

Call methods directly within the same class.
public class MyService
{
    public void DoWork()
    {
        HelperMethod(); // Correct method call
    }

    public void HelperMethod()
    {
        // Implementation
    }
}