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.
Recommended 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
CancellationTokenparameters. 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
tryblock and handle exceptions in acatchblock. -
Finally Block: Use
finallyto execute cleanup code, regardless of whether an exception occurred. -
Throw Statement: Use
throwto 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
-
Define async methods: Mark a method as
asynconly if it contains at least oneawait. -
Always use await: Avoid writing
asyncmethods withoutawait—this results in synchronous execution and compiler warnings. -
Avoid async void: Always return
TaskorTask<T>from async methods.
public async Task<string> GetDataAsync()
{
using (var client = new HttpHelper())
{
return await client.GetStringAsync("http://example.com/data");
}
}
.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
}
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.
Steps to Implement
Locate the Post-Build Script Navigate to the
postbuildscript.ps1file in your project directory.- Update the Script Replace the entire content of
postbuildscript.ps1with 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 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.nugetpackage 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 IValidationCallback → BeforePricingValidationAsync
-
You cannot access child objects of
LineItem. -
Use
SetToRePricing()to mark aLineItemfor repricing.
For IPricingBasePriceCallback → BeforePricingBatchAsync
-
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.
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
HelperMethodare instance methods that can be called directly (HelperMethod();), which may confuse readers about how to correctly use the API.
Correct Approach
public class MyService
{
public void DoWork()
{
HelperMethod(); // Correct method call
}
public void HelperMethod()
{
// Implementation
}
}
