How modern C# features transform legacy Acumatica code into elegant and efficient solutions through optimization
This content was prepared by a human using a text editor, Visual Studio 2026, AI tools, and materials from open Internet sources.
In Part 3, we'll explore the following C# language features:
- Extension methods with modern style — expression-bodied, target-typed new
- Local functions — functions inside methods
- Implicit var everywhere —
varfor local variables - Null-coalescing improvements (
??,??=) — defaults and null protection - Tuple patterns + when guards — more complex pattern matching constructs with conditions
Let's dive in.
11. Extension methods (advanced usage)
C# / .NET Version
- C# 3.0 (always available)
- But modern practices leverage expression-bodied members + target-typed new + switch expressions
Purpose
- Enriches types with convenient utilities
- Enables declarative code
- Reduces the number of static helper classes
C# Example
/// <summary>
/// Extensions for common string operations following a modern C# style.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Trims whitespace and returns an empty string when input is null/whitespace.
/// </summary>
public static string NormalizeSpace(this string? value) =>
// If input is null or consists only of whitespace return empty, otherwise trim edges.
string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
/// <summary>
/// Truncates the string to the specified maximum length.
/// If the string is shorter than or equal to <paramref name="max"/>, returns the original string.
/// </summary>
public static string Truncate(this string value, int max) =>
// Guard: return original when length within limits, otherwise substring up to max.
value.Length <= max ? value : value.Substring(0, max);
}
/// <summary>
/// Numeric helper extensions.
/// </summary>
public static class NumberExtensions
{
/// <summary>
/// Returns true when <paramref name="value"/> is between <paramref name="min"/> and <paramref name="max"/>, inclusive.
/// </summary>
public static bool IsBetween(this int value, int min, int max) =>
// Simple inclusive range check.
value >= min && value <= max;
}
/// <summary>
/// General-purpose helper extensions for objects.
/// </summary>
public static class ObjectExtensions
{
/// <summary>
/// Applies a function <paramref name="fn"/> to <paramref name="obj"/> and returns the result.
/// Useful for fluent transformations.
/// </summary>
public static TResult? Let<T, TResult>(this T obj, Func<T, TResult> fn) =>
// Execute provided function and return its result.
fn(obj);
}
// Usage examples
// Normalize and truncate a string (modern helper usage)
var s = " hello ".NormalizeSpace().Truncate(4);
// Numeric range check
int qty = 5;
bool ok = qty.IsBetween(1, 10);
// Apply a function to a value and retrieve the result (Let pattern)
int? len = "hello".Let(x => x.Length);
Acumatica Example (optimized)
/// <summary>
/// Extensions for SOOrder domain behavior (modern style examples).
/// </summary>
public static class SOOrderExtMethods
{
/// <summary>
/// Returns true when the order status indicates the order can be edited.
/// Uses the helper <see cref="IsIn"/> to check multiple statuses.
/// </summary>
public static bool IsEditable(this SOOrder order) =>
order.Status.IsIn(SOOrderStatus.Open, SOOrderStatus.Hold);
/// <summary>
/// Returns true when the order is completed.
/// </summary>
public static bool IsCompleted(this SOOrder order) =>
order.Status == SOOrderStatus. Completed;
}
/// <summary>
/// Graph related helper methods.
/// </summary>
public static class GraphExtMethods
{
/// <summary>
/// Ensures <paramref name="value"/> is not null. Throws a <see cref="PXException"/> with a clear message when missing.
/// Returns the non-null value on success.
/// </summary>
public static T Require<T>(this PXGraph graph, T? value, string name)
{
if (value == null)
// Throw when the required object is missing in the graph context.
throw new PXException($"{name} is missing");
return value;
}
}
/// <summary>
/// Query convenience extensions using the BQL fluent API.
/// </summary>
public static class QueryExtensions
{
/// <summary>
/// Fetches a single <see cref="SOOrder"/> by order number in a read-only view.
/// Returns null when no record is found.
/// </summary>
public static SOOrder? FetchSingle(this PXGraph graph, string orderNbr) =>
SelectFrom<SOOrder>
.Where<SOOrder.orderNbr.IsEqual< @P.AsString>>
.View.ReadOnly
.Select(graph, orderNbr);
}
/// <summary>
/// Field helper extensions providing safe operations for nullable strings.
/// </summary>
public static class FieldExtensions
{
/// <summary>
/// Returns <paramref name="def"/> when <paramref name="value"/> is null or whitespace, otherwise returns the original value.
/// </summary>
public static string Safe(this string? value, string def = "") =>
string.IsNullOrWhiteSpace(value) ? def : value;
}
// Usage of extension methods
// Acumatica-specific examples require a valid graph context + data; create a graph instance.
PXGraph graph = PXGraph.CreateInstance<PXGraph>();
// Use the fluent query helper to fetch a single order by number. Result may be null.
var order = graph.FetchSingle("SO000123");
// If the order exists but is not editable, throw a domain-specific exception.
if (order != null && !order.IsEditable())
throw new PXException("Order not editable");
// Demonstrate Safe helper with a possibly null input.
string? name = null;
string safeName = name.Safe("Unknown");
Acumatica Example (not optimized)
/// <summary>
/// Old-style helper for PXSelect families returning a single or null result.
/// </summary>
public static class PXSelectExtensions
{
/// <summary>
/// Executes the view and returns the first result or null when none exist.
/// </summary>
public static T SelectSingleOrNull<T>(this PXSelectBase<T> view)
where T : class, IBqlTable, new()
{
// Execute the view and take the first row if any.
return view.Select().FirstOrDefault();
}
}
/// <summary>
/// AR Invoice domain helper methods (old style examples).
/// </summary>
public static class ARInvoiceExtensions
{
/// <summary>
/// Returns true when the invoice is a draft (not released and on hold).
/// </summary>
public static bool IsDraft(this ARInvoice inv) =>
inv.Released != true && inv.Hold == true;
/// <summary>
/// Returns true when the invoice is ready for release (not released and not on hold).
/// </summary>
public static bool IsReadyForRelease(this ARInvoice inv) =>
inv.Released != true && inv.Hold != true;
}
/// <summary>
/// String-related helpers mirroring older utility patterns.
/// </summary>
public static class PXStringExtensions
{
/// <summary>
/// Trims and returns empty string when input is null.
/// </summary>
public static string TrimOrEmpty(this string? value) =>
value?.Trim() ?? string.Empty;
/// <summary>
/// Returns true when the string contains non-whitespace characters.
/// </summary>
public static bool IsFilled(this string? value) =>
!string.IsNullOrWhiteSpace(value);
}
/// <summary>
/// Null-check helpers that throw when a required value is missing.
/// </summary>
public static class NullExtensions
{
/// <summary>
/// Throws a <see cref="PXException"/> with <paramref name="message"/> when <paramref name="value"/> is null.
/// Otherwise returns the value.
/// </summary>
public static T ThrowIfNull<T>(this T? value, string message)
{
if (value == null)
throw new PXException(message);
return value;
}
}
// Usage of extension methods above
// 1) SelectSingleOrNull
ARInvoice firstInvoice = Invoices.SelectSingleOrNull();
if (firstInvoice == null)
{
PXTrace.WriteInformation("No invoices found");
}
// 2) IsDraft
if (firstInvoice != null && firstInvoice.IsDraft())
{
PXTrace.WriteInformation($"Invoice {firstInvoice.RefNbr} is a draft");
}
// 3) IsReadyForRelease
if (firstInvoice != null && firstInvoice.IsReadyForRelease())
{
PXTrace.WriteInformation($"Invoice {firstInvoice.RefNbr} is ready for release");
}
// 4) TrimOrEmpty
string raw = " ABC ";
string cleaned = raw.TrimOrEmpty();
PXTrace.WriteInformation($"Cleaned value: '{cleaned}'");
string nullValue = null;
string cleanedNull = nullValue.TrimOrEmpty();
PXTrace.WriteInformation($"Cleaned NULL: '{cleanedNull}'");
// 5) IsFilled
string filled = "hello";
string empty = "";
string nullStr = null;
PXTrace.WriteInformation($"filled.IsFilled(): {filled.IsFilled()}");
PXTrace.WriteInformation($"empty.IsFilled(): {empty.IsFilled()}");
PXTrace.WriteInformation($"nullStr.IsFilled(): {nullStr.IsFilled()}");
// 6) ThrowIfNull
ARInvoice maybeInvoice = null;
try
{
// will throw PXException
var inv = maybeInvoice.ThrowIfNull("Invoice is required!");
}
catch (PXException ex)
{
PXTrace.WriteInformation($"Caught error: {ex.Message}");
}
12. Local functions
C# / .NET Version
- C# 7.0
- Works in .NET Framework 4.8
Purpose
- Helps decompose large methods
- Private logic remains inside the method
- Improves readability
C# Example
/// Demonstrates a local function inside a method and calling it inside a loop.
/// Useful to factor small reusable pieces of logic scoped to the method.
/// </summary>
private void PrintNumbers(int n)
{
void Print(int x)
{
Console.WriteLine($"Number {x}");
}
for (int i = 1; i <= n; i++) Print(i);
}
Acumatica Example (optimized)
public void ProcessCompleted()
{
foreach (var o in Completed())
PXTrace.WriteInformation(o.OrderNbr);
IEnumerable<SOOrder> Completed() =>
SelectFrom<SOOrder>.Where<SOOrder.status.IsEqual<SOOrderStatus.completed>>.View
.Select(this)
.RowCast<SOOrder>();
}
Acumatica Example (not optimized)
private void PrintNumbers(int n)
{
foreach (var o in SelectOrders())
PXTrace.WriteInformation(o.OrderNbr);
}
private IEnumerable<SOOrder> SelectOrders()
{
return SelectFrom<SOOrder>
.Where<SOOrder.status.IsEqual<SOOrderStatus.completed>>
.View
.Select(this)
.RowCast<SOOrder>();
}
13. Implicit var everywhere
C# / .NET Version
- C# 3.0
- Used more frequently in modern styles, pairs well with target-typed new
Purpose
- Reduces visual noise
- Eliminates type duplication
- Doesn't compromise readability when the right-hand side is clear
C# Example
// C# new style, Implicit var
var name = "Acme Corp";
var orderCount = 42;
var orders = new List<string> { "ORD001", "ORD002", "ORD003" };
foreach (var order in orders)
{
Console.WriteLine($"{name} has order {order}");
}
Acumatica Example (optimized)
private void UseVar()
{
// Create SOOrderEntry graph - modern, correct approach
var graph = PXGraph.CreateInstance<SOOrderEntry>();
// Current record (may be null - that's fine)
var o = graph.Document.Current;
// Implicit typing for collections
var messages = new List<string>();
// The ?? operator - also modern C#
var amount = o?.OrderTotal ?? 0m;
PXTrace.WriteInformation($"Order amount = {amount}");
// But don't do this - bad example. Type is unclear
var result = GetResult();
// Do this instead
OrderStatus result = GetResult();
}
Acumatica Example (not optimized)
// Acumatica old style, explicit types
string name = "Acme Corp";
int orderCount = 42;
List<string> orders = new List<string> { "ORD001", "ORD002", "ORD003" };
foreach (string order in orders)
{
PXTrace.WriteInformation(name + " has order " + order);
}
14. Nullable-aware operators (?., ??, ??=)
C# / .NET Version
- C# 6.0 / 7.0
Purpose
- Enables proper
nullhandling - Reduces
if (x != null)checks - Standardizes defensive coding
C# Example
// Nullable-aware operators in C#
class Customer
{
public string Name { get; set; }
public string Email { get; set; }
}
void Demo()
{
Customer cust = null;
// ?. — safe property access
string name = cust?.Name;
// ?? — default value
string displayName = cust?.Name ?? "Unknown";
// ??= — assign only if null
string email = null;
email ??= "noemail@domain.com";
Console.WriteLine($"Name: {name}, Display: {displayName}, Email: {email}");
}
Acumatica Example (optimized)
private void DemoNullableOperators()
{
Customer cust = Base.CurrentCustomer?.Current; // ?. safe access to object in graph
string name = cust?.Name;
string displayName = cust?.Name ?? "Unknown";
string email = null;
email ??= "noemail@domain.com";
PXTrace.WriteInformation($"Name: {name}, Display: {displayName}, Email: {email}");
}
Acumatica Example (not optimized)
private void DemoNullableOperators()
{
Customer cust = Base.CurrentCustomer.Current;
// ?. replaced with regular checks
string name = null;
if (cust != null)
name = cust.Name;
// ?? replaced with regular ternary
string displayName = (cust != null && cust.Name != null) ? cust.Name : "Unknown";
// ??= replaced with null check before assignment
string email = null;
if (email == null)
email = "noemail@domain.com";
PXTrace.WriteInformation("Name: " + name + ", Display: " + displayName + ", Email: " + email);
}
15. Tuple patterns + when-guards
C# / .NET Version
- C# 8.0
Purpose
- Allows combining data into a single pattern
- Makes switch constructs declarative
- Adds conditions (
when) directly in the pattern
C# Example
// Tuple patterns + when-guards
void DemoTuplePatterns(int x, int y)
{
var result = (x, y) switch
{
(0, 0) => "Both zero",
(0, _) => "X is zero",
(_, 0) => "Y is zero",
var (a, b) when a == b => "Equal values",
_ => "Different values"
};
Console.WriteLine(result);
}
Acumatica Example (optimized)
private string DescribeOrder(SOOrder order)
{
return (order.Status, order.ShipComplete) switch
{
(SOOrderStatus.Open, SOShipComplete.BackOrderAllowed) => "Partial Shipping Allowed",
(SOOrderStatus.Open, SOShipComplete.ShipComplete) => "Must Ship Complete",
(SOOrderStatus.Hold, _) => "On Hold",
(_, _) => "Unknown"
};
}
Acumatica Example (not optimized)
private string DescribeOrder_Old(SOOrder order)
{
if (order.Status == SOOrderStatus.Open && order.ShipComplete == SOShipComplete.BackOrderAllowed)
return "Partial Shipping Allowed";
if (order.Status == SOOrderStatus.Open && order.ShipComplete == SOShipComplete.ShipComplete)
return "Must Ship Complete";
if (order.Status == SOOrderStatus.Hold)
return "On Hold";
return "Unknown";
}
Over recent years, C# has evolved from just "another OOP language" into a powerful tool that genuinely helps you write better code. In the Acumatica context, this means:
Business benefits:
- Bug reduction of 20-40% through null-safety and immutability
- Development speed increase of 25% thanks to compact syntax
- Simplified onboarding of new developers through code readability
The most impactful changes:
- Null-coalescing operators — instant stability improvement
- Pattern matching — radical simplification of complex conditions
- Manual Record DTO — elegant solution for integrations
- Dictionary + enum — performant state machines
Good luck with modernization! Your users will appreciate the stability, and your team will enjoy working with clean code. What modern C# features do you use in your Acumatica projects? Share your examples in the comments!