Skip to main content

Modern C# in Acumatica: Refactoring for Optimal Code Clarity and Performance. Part 3.

  • November 27, 2025
  • 0 replies
  • 34 views

Forum|alt.badge.img+1

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:

  1. Extension methods with modern style — expression-bodied, target-typed new
  2. Local functions — functions inside methods
  3. Implicit var everywhere — var for local variables
  4. Null-coalescing improvements (????=) — defaults and null protection
  5. 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 null handling
  • 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:

  1. Null-coalescing operators — instant stability improvement
  2. Pattern matching — radical simplification of complex conditions
  3. Manual Record DTO — elegant solution for integrations
  4. 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!