Loading ...

Multiple Implementations of a Generic Graph Extension in One Graph in Acumatica ERP 2026 R1

One of the most powerful techniques for reusing business logic in Acumatica is the use of Generic Graph Extensions. They allow developers to build functionality once and apply it across multiple graphs and DACs.

Until recently, there was one limitation that I occasionally ran into when designing reusable components. If the same graph needed multiple implementations of a generic extension with different mappings, the framework didn't provide a clean way to do it.

Acumatica ERP 2026 R1 changes that.

Why Does This Matter?

Imagine you have a generic extension that calculates pricing information. One DAC stores estimated values and another stores actual values.

Before 2026 R1, reusing the same generic extension for both DACs within a single graph was not straightforward. Developers often had to duplicate code, create additional abstraction layers, or implement custom workarounds to achieve the desired behavior.

With Acumatica ERP 2026 R1, the framework now supports multiple implementations of the same generic graph extension within a single graph, each using its own DAC mappings.

What Has Changed in 2026 R1?

Before 2026 R1

public abstract class SalesPriceGraph<TGraph, TPrimary>

Acumatica ERP 2026 R1

public abstract class SalesPriceGraph<TGraph, TDocument, TDetail>

The key difference is that the generic extension now accepts additional type parameters for mapped DACs. This allows the framework to distinguish between multiple implementations of the same extension and use different mappings within the same graph.

Building a Practical Example

To demonstrate the new capability introduced in Acumatica ERP 2026 R1, I created a simple validation scenario.

The business rule is intentionally straightforward: the quantity entered on a document line must be greater than 5.

The goal of the example is not the validation itself. Instead, it demonstrates how the same generic graph extension can be implemented multiple times within a single graph while using different DAC mappings.

First, I created a generic graph extension that contains reusable validation logic.

public abstract class ValidationGraph<TGraph, TDocument, TDetail> :

      PXGraphExtension<TGraph>

      where TGraph : PXGraph

      where TDocument : class, IBqlTable, new()

      where TDetail : class, IBqlTable, new(){}

The extension contains a mapped cache extension named Detail. This mapped extension defines a generic quantity field that will later be mapped to actual DAC fields.

public class Detail : PXMappedCacheExtension

 {

     public abstract class qty : BqlDecimal.Field<qty> { }

     public decimal? Qty { get; set; }

 }

The mapping between the generic field and a real DAC field is defined through the DetailMapping class.

protected class DetailMapping : IBqlMapping

{

    public Type Extension => typeof(Detail);

 

    protected Type _table;

    public Type Table => _table;

 

    public Type Qty = typeof(Detail.qty);

 

    public DetailMapping(Type table)

    {

        _table = table;

    }

}

The validation logic itself is implemented only once.

protected virtual void _(Events.FieldVerifying<TDetail, Detail.qty> e)

{

    if (e.NewValue is not decimal qty)

    {

        return;

    }

 

    if (qty < 5m)

    {

        throw new PXSetPropertyException<Detail.qty>(Messages.QuantityIsTooSmall, PXErrorLevel.Error, qty);

    }

}

The generic graph extension also defines the GetDetailMapping() method.

protected virtual DetailMapping GetDetailMapping()

 {

     return new DetailMapping(typeof(TDetail));

 }

This method provides the default mapping for the generic detail DAC specified by the TDetail type parameter.

The extension does not know which DAC it will work with. It only validates the generic Detail.qty field.

The actual DAC mapping is provided by implementation classes.

For Sales Orders, the generic quantity field is mapped to SOLine.orderQty.

public class SOLineValidation :ValidationGraph<SOOrderEntry, SOOrder, SOLine>

 {

     public static bool IsActive() => true;

 

     protected override DetailMapping GetDetailMapping()

     {

         return new DetailMapping(typeof(SOLine))

         {

             Qty = typeof(SOLine.orderQty)

         };

     }

 }

A second implementation of the same generic graph extension is created for SOLineSplit.

public class SOLineSplitValidation :ValidationGraph<SOOrderEntry, SOOrder, SOLineSplit>

 {

     public static bool IsActive() => true;

 

     protected override DetailMapping GetDetailMapping()

     {

         return new DetailMapping(typeof(SOLineSplit))

         {

             Qty = typeof(SOLineSplit.qty)

         };

     }

 }

 

Both implementation classes inherit from the same generic graph extension and are attached to the same graph (SOOrderEntry), but each implementation uses a different DAC mapping.

As a result, the validation logic is written only once while being reused for both SOLine and SOLineSplit records.

When a user enters a quantity lower than 5 on a Sales Order line, the validation is executed and the framework displays an error message.



 

Extending the Generic Extension with One More Validation Rule

Suppose that after implementing quantity validation, a new business requirement appears. Users should not be allowed to enter a Ship Date that is earlier than the current business date.

Because the validation logic is already centralized in the generic graph extension, no changes are required in the implementation classes other than updating the field mappings. The new validation rule can be added directly to the generic extension and automatically reused by all implementations.

First, we extend the mapped cache extension by adding a generic Ship Date field.

 

public class Detail : PXMappedCacheExtension

 {

     public abstract class qty : BqlDecimal.Field<qty> { }

     public decimal? Qty { get; set; }

 

     public abstract class shipDate : BqlDateTime.Field<shipDate> { }

     public DateTime? ShipDate { get; set; }

 }

Next, we update the mapping class.

protected class DetailMapping : IBqlMapping

 {

     public Type Extension => typeof(Detail);

 

     protected Type _table;

     public Type Table => _table;

 

     public Type Qty = typeof(Detail.qty);

     public Type ShipDate = typeof(Detail.shipDate);

 

     public DetailMapping(Type table)

     {

         _table = table;

     }

 }

The new validation rule is then implemented once in the generic graph extension.

 

protected virtual void _(Events.FieldVerifying<TDetail, Detail.shipDate> e)

 {

     if (e.NewValue is DateTime shipDate &&

         Base.Accessinfo.BusinessDate is DateTime businessDate &&

         shipDate.Date < businessDate.Date)

     {

         throw new PXSetPropertyException<Detail.shipDate>(Messages.ShipDateCannotBeEarlierThenBD, PXErrorLevel.Error);

     }

 }

Finally, the implementation classes only need to map the new field.

For Sales Order lines:

public class SOLineValidation : ValidationGraph<SOOrderEntry, SOOrder, SOLine>

{

    public static bool IsActive() => true;

 

    protected override DetailMapping GetDetailMapping()

    {

        return new DetailMapping(typeof(SOLine))

        {

            Qty = typeof(SOLine.orderQty),

            ShipDate = typeof(SOLine.shipDate)

        };

    }

}

For Sales Order line splits:

public class SOLineSplitValidation :ValidationGraph<SOOrderEntry, SOOrder, SOLineSplit>

{

    public static bool IsActive() => true;

 

    protected override DetailMapping GetDetailMapping()

    {

        return new DetailMapping(typeof(SOLineSplit))

        {

            Qty = typeof(SOLineSplit.qty),

            ShipDate = typeof(SOLineSplit.shipDate)

        };

    }

}

As a result, both implementations automatically receive the new validation behavior without duplicating any business logic. The implementation classes continue to contain only DAC-specific mappings, while all validation rules remain centralized in the generic graph extension.


Be the first to rate this post

  • Currently 0.0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5