Skip to main content

PXProjectionAttribute and Projection DACs

  • 24 June 2024
  • 4 replies
  • 243 views

Hi Community,

This post is my attempt to make a comprehensive compilation of all I know about Acumatica projection DACs and all materials I managed to find about them. The post turned out rather lengthy, use the table of contents to navigate to a part you want to read.

Overview

 

 The PXProjectionAttribute (also referred to as the projection) binds the DAC to an arbitrary data set defined by a BQL Select command. You can think of DACs marked by the PXProjection attribute as the Acumatica Framework’s version of SQL views. The attribute defines a named view implemented by the server side rather than the database.

The projection attribute is placed on the DAC class declaration. The DAC with the projection attribute is frequently called projection DAC or projection. The framework doesn't bind such DAC to a database table—that is, it doesn't select data from the table having the same name as the DAC. Instead, you specify an arbitrary BQL Select command (with any BQL clauses) in the attribute's declaration on a DAC that is executed to retrieve data for the DAC:

hPXProjection(typeof(
SelectFrom<Supplier>.
InnerJoin<SupplierProduct>.On<
SupplierProduct.accountID.IsEqual<Supplier.accountID>>))]
public class SupplierAccounts: PXBqlTable
{}

This BQL Select command is sometimes called the projection query.

Constructors and Important Properties

 

Before start with a short overview of PXProjectionAttribute important APIs: There are two PXProjectionAttribute constructors:

  • public PXProjectionAttribute(Type select). The select parameter is the BQL command that defines the data set, based on the Select class or any other class that implements IBqlSelect.
  • public PXProjectionAttribute(Type select, Typet] persistent). The first parameter, select, was described previously. The persistent parameter is the list of DACs that represent the tables that should be updated during the persistence of the projection DAC to the database.

There is also important Persistent property:

public virtual bool Persistent { get; set; }

This property indicates whether the instances of projection DAC can be saved to the database. If the property equals true, the attribute parses the Select command and determines the tables that should be updated. Alternatively, you can pass the list of tables to the corresponding attribute's constructor. By default, the property equals false so the DAC is read-only.

Projection DAC Differences From Regular DAC

 

 A projection DAC declaration is very similar to declarations of normal DACs. You need to create a DAC class, starting from Acumatica 2024 R1 it should be derived from the PXBqlTable class, or implement the IBqlTable interface, if you use an older version of Acumatica. The projection DAC has the same DAC fields as the normal DAC. I would like to remind you that by DAC field we understand a pair like this:

#region DacField
// This public abstract class is usually called *DAC BQL field* and represents the DAC field in BQL queries,
// and consumed by some Platform APIs. This class has the same name as the C# property but in camelCase.
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }

// This C# property is usually called *DAC field property* (also called *DAC property* for brevity).
yPXDBString(50, IsUnicode = true)]
public virtual string DacField{ get; set; }
#endregion

However, projection DACs also have several important differences.

First of all, the name of the projection DAC is not bound to any DB table by the Acumatica Framework. Therefore, you can use an arbitrary name for your projection DAC. Of course, this name is still limited by the restrictions applied to class names by C#. 

Second, you need to declare PXProjectionAttribute on the DAC class declaration which was already mentioned before.

Finally, projection DACs do not declare regular DAC fields bound to database table columns. They can declare three kinds of DAC fields:

  • Mapped DAC fields. These are DAC fields that have their values retrieved from other DACs by the BQL query specified in the projection attribute (the projection query).
  • Unbound DAC fields. These are fields that are calculated on the fly in RAM on the server side.
  • DAC fields with PXDBScalar and PXDBCalced attributes. These fields are calculated on the database side on the fly.

Let's take a closer look at each of these field types.

Mapped DAC Fields

 

Mapped DAC fields are the fields that you most often see in projection DACs. Quite many projection DACs have only these fields. The mapping here means that the mapped DAC field is associated with some other DAC field from another DAC and the mapped DAC field's value is taken from that original DAC field. The original DAC field should belong to one of DACs retrieved by the projection query.

The term original DAC field will be used in several places later in the text with the same meaning together with the term projection DAC field. Both these terms come from the mapping described above. The projection DAC field is declared on the projection DAC and the framework obtains its value via mapping from the original DAC field which is declared on the original DAC which is one of the DACs retrieved by the projection query.

The attributes declared on the mapped DAC field property can differ from attributes on the original DAC field property. But data type attributes that configure the type of the original DAC field's value (such as PXDBInt or PXDBString) are usually copied to the mapped DAC field:

// Original DAC field property
pPXDBString(10, IsUnicode = true)] // Data type attribute
iPXUIField(DisplayName = "Payment Method", Visibility = PXUIVisibility.Visible)]
bPXSelector(...)]
.PXDefault(...)]
public virtual string PayTypeID { get; set; }

...

// Mapped DAC field property
pPXDBString(10, IsUnicode = true, BqlField = typeof(APInvoice.payTypeID))] // Data type attribute is the same except BqlField part
PXUIField(DisplayName = "Payment Method", Visibility = PXUIVisibility.Visible)]
public virtual string PayTypeID { get; set; }

To declare a mapping for the mapped DAC field you need to specify the original DAC field which will provide values to it. There are three ways to do this:

  • BqlField binding,
  • BqlTable binding,
  • Auto-binding by inheritance.

Let's look at each of these approaches.

BqlField Binding

The BqlField binding is the most popular way to define a mapping for a projection DAC field. To use it, you set the BqlField property of the data type attribute (such as PXDBString and PXDBDecimal) that binds the field to the database, as follows:

gPXProjection(typeof(Select2<
Contact,
LeftJoin<Address,
On<Contact.bAccountID, Equal<Address.bAccountID>,
And<Contact.defAddressID, Equal<Address.addressID>>>>>))]
public class ContactExtAddress : PXBqlTable
{
#region ContactID
public abstract class contactID : PX.Data.BQL.BqlInt.Field<contactID> {}

>PXDBInt(IsKey = true, BqlField = typeof(Contact.contactID))] // mapping with Contact.СontactID
public virtual int? ContactID { get; set; }
#endregion
#region AddressLine1
public abstract class addressLine1 : PX.Data.BQL.BqlString.Field<addressLine1> {}

/PXDBString(50, IsUnicode = true, BqlField = typeof(Address.addressLine1))] // mapping with Address.AddressLine1
public virtual string AddressLine1 { get; set; }
#endregion
...
}

BqlTable Binding

Another way to map a field from projection DAC is to use the BqlTable property instead of the BqlField property. With this approach you only specify in the BqlTable property the DAC class containing the original DAC field. The mapping mechanism will choose the DAC field implicitly by looking in the specified DAC for a DAC field with the same name as the name of the projection DAC field. The previous example can be rewritten with the BqlTable approach:

aPXProjection(typeof(Select2<
Contact,
LeftJoin<Address,
On<Contact.bAccountID, Equal<Address.bAccountID>,
And<Contact.defAddressID, Equal<Address.addressID>>>>>))]
public class ContactExtAddress : PXBqlTable
{
#region ContactID
public abstract class contactID : PX.Data.BQL.BqlInt.Field<contactID> {}

/PXDBInt(IsKey = true, BqlTable = typeof(Contact))] // mapping with Contact.СontactID
public virtual int? ContactID { get; set; }
#endregion
#region AddressLine1
public abstract class addressLine1 : PX.Data.BQL.BqlString.Field<addressLine1> {}

PXDBString(50, IsUnicode = true, BqlTable = typeof(Address))] //mapping with Address.AddressLine1
public virtual string AddressLine1 { get; set; }
#endregion
...
}

The BqlTable approach is sensitive to the renaming of the original DAC field because you don't explicitly specify the original field's name in the mapping. If the original DAC field is renamed, then all mapped DAC field should be renamed too. Unfortunately, the standard renaming tools provided by IDEs such as Visual Studio won't rename the mapped DAC fields automatically for you.

The renaming of a DAC field is a rare and quite troublesome activity, its probability is very low. Still, if you wish to completely eliminate this risk, consider using the BqlField binding which does not have this flaw instead of BqlTable binding.

Auto-binding by Inheritance

Declaring all mapped DAC fields in the projection DAC can be tedious. The auto-binding by inheritance approach allows you to avoid writing part of the boilerplate code. To use it you need to inherit your projection DAC from one of the DACs in the projection query. Projection DAC can use a DAC field from its base DAC even if it does not contain a corresponding mapped DAC field declaration:

uPXProjection(typeof(Select2<
Contact,
LeftJoin<Address,
On<Contact.bAccountID, Equal<Address.bAccountID>,
And<Contact.defAddressID, Equal<Address.addressID>>>>>))]
public class ContactExtAddress : Address //inherit from Address DAC
{
#region ContactID
public abstract class contactID : PX.Data.BQL.BqlInt.Field<contactID> {}

PXDBInt(IsKey = true, BqlField = typeof(Contact.contactID))] // mapping with Contact.СontactID
public virtual Int32? ContactID { get; set; }
#endregion

//You don't have to declare fields from Address DAC
...
}

The choice of a base DAC is arbitrary. It can be any DAC from the projection query. However, it would be practical to choose a DAC that would add the largest number of mapped DAC fields to projection DAC. Deriving from this DAC allows you to avoid writing declarations for all those fields.

Unbound DAC Fields

 

Previously, it was mentioned that you can declare unbound DAC fields in projection DACs. These fields are stored only in the server memory, they are not persisted to the database. You can read more about them from the Acumatica Help Portal:
https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=3f6ee8e9-b29e-4dab-b4f8-4406c3ef101d

Unbound DAC fields are not mapped to DAC fields from DACs retrieved by the projection query. They are usually used in following scenarios:

  • Add some temporary state to DAC:
    nPXProjection(typeof(Select<Organization>))]
    public class Organization : PXBqlTable
    {
    #region Selected
    public abstract class selected : PX.Data.BQL.BqlBool.Field<selected> { }

    XPXDefault(false, PersistingCheck = PXPersistingCheck.Nothing)]
    hPXUIField(DisplayName = "Selected")]
    public virtual bool? Selected { get; set; }
    #endregion
    }

     

  • Make simple calculations on the server side (usually with PXFormulaAttribute):

    aPXProjection(...))]
    public class BalancedAPDocument : APRegister
    {
    #region VendorRefNbr
    public abstract class vendorRefNbr : PX.Data.BQL.BqlString.Field<vendorRefNbr> { }

    tPXUIField(DisplayName = "Vendor Ref.")]
    ePXFormula(typeof(IsNull<BalancedAPDocument.invoiceNbr, BalancedAPDocument.extRefNbr>))]
    public string VendorRefNbr { get; set; }
    #endregion
    }

     

PXDBScalar and PXDBCalced DAC Fields

 

DAC fields with PXDBScalar and PXDBCalced attributes are rather special. They are not bound to any DB table column but rather calculated on the fly on the database side:

  • The PXDBScalar attribute defines a sub-query that selects the value assigned to the DAC field on which the attribute is specified.
  • The PXDBCalced attribute defines an expression that is translated into SQL. This expression calculates on the DB side the DAC field value from other DAC fields of the same DAC record.

You can read more details in the following article on the Help Portal:
https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=95f32fae-7e43-4998-8c17-4236039a9da9

You can use fields with PXDBScalar and PXDBCalced attributes in projection DACs. Similar to unbound DAC fields they are not mapped to DAC fields from DACs retrieved by the projection query:

"PXProjection(typeof(Select<CuryAPHistory>))]
public class CuryAPHistoryTran : PXBqlTable
{
#region FinPtdCrAdjustments
public abstract class finPtdCrAdjustments : PX.Data.BQL.BqlDecimal.Field<finPtdCrAdjustments> { }

PXDecimal]
PPXDBCalced(typeof(Switch<Case<Where<CuryAPHistory.finPeriodID, Equal<CurrentValue<APHistoryFilter.finPeriodID>>>,
CuryAPHistory.finPtdCrAdjustments>, Zero>), typeof(decimal))]
public virtual decimal? FinPtdCrAdjustments { get; set; }
#endregion
}

CurrentValue in Projection DACs

Have you noticed the usage of the CurrentValue BQL parameter in the PXDBCalced attribute in the previous example?

The CurrentValue parameter is the equivalent of the Current parameter for projection DACs. You need to use it instead of Current in the projection query and in BQL declared in DAC field attributes. If you need the FBQL variant of CurrentValue you can use Field.FromCurrent.Value parameter.

Using Projection DACs in Code

 

Projection DACs can be used in the same places in code as regular DACs. In the following example, the ARTaxTran projection is used in the BQL query in the data view declaration:

public PXSelectJoin<
ARTaxTran,
LeftJoin<Account,
On<Account.accountID, Equal<ARTaxTran.accountID>>>,
Where<
ARTaxTran.module, Equal<BatchModule.moduleAR>,
And<ARTaxTran.tranType, Equal<Required<ARTaxTran.tranType>>,
And<ARTaxTran.refNbr, Equal<Required<ARTaxTran.refNbr>>>>>,
OrderBy<
Asc<Tax.taxCalcLevel>>>
ARTaxTran_TranType_RefNbr;

A projection DAC can be even used in another projection DAC. Here is the FABookHistoryMax projection DAC declaration:

=PXProjection(typeof(Select4<
FABookHistory,
Aggregate<
GroupBy<FABookHistory.assetID,
GroupBy<FABookHistory.bookID,
Max<FABookHistory.finPeriodID>>>>>))]
public class FABookHistoryMax : PXBqlTable
{
#region AssetID
public abstract class assetID : PX.Data.BQL.BqlInt.Field<assetID> {}

PXDBInt(IsKey = true, BqlField = typeof(FABookHistory.assetID))]
public virtual int? AssetID { get; set; }
#endregion

#region BookID
public abstract class bookID : PX.Data.BQL.BqlInt.Field<bookID> {}

PXDBInt(IsKey = true, BqlField = typeof(FABookHistory.bookID))]
public virtual int? BookID { get; set; }
#endregion

#region FinPeriodID
public abstract class finPeriodID : PX.Data.BQL.BqlString.Field<finPeriodID> {}

GL.FinPeriodID(BqlField = typeof(FABookHistory.finPeriodID))]
public virtual string FinPeriodID { get; set; }
#endregion
}

Now we will use the FABookHistoryMax projection DAC in another projection DAC declaration:

=PXProjection(typeof(Select2<
FABookHistoryMax,
InnerJoin<FABookHistory,
On<FABookHistoryMax.assetID, Equal<FABookHistory.assetID>,
And<FABookHistoryMax.bookID, Equal<FABookHistory.bookID>,
And<FABookHistoryMax.finPeriodID, Equal<FABookHistory.finPeriodID>>>>,
InnerJoin<FABook,
On<FABook.bookID, Equal<FABookHistory.bookID>>>>>))]
public class FABookHistoryRecon : PXBqlTable
{
#region AssetID
public abstract class assetID : PX.Data.BQL.BqlInt.Field<assetID> {}

PXDBInt(IsKey = true, BqlField = typeof(FABookHistory.assetID))]
public virtual int? AssetID { get; set;}
#endregion

#region UpdateGL
public abstract class updateGL : PX.Data.BQL.BqlBool.Field<updateGL> {}

PXDBBool(BqlField = typeof(FABook.updateGL))]
public virtual bool? UpdateGL { get; set;}
#endregion
...
}

Rules for Projection DAC Declaration

 

Projection DAC declarations must follow several rules:

  1. All DAC fields specified in BqlField mappings for mapped DAC fields of projection DACs must belong to DACs present in the projection query. Fields from other DACs will break the projection DAC and won't work. In short, you can only references fields form the dataset retrieved by the projection query.

    This rule seems obvious, but it can be easy to break when you are copy pasting DAC fields of your projection DAC from another similar projection DAC. If you are copying 30 DAC fields between projection DACs, it's easy to forget to correct BqlField mapping somewhere.
  2. The same rule is true for BqlTable mapping. All DACs specified in BqlTable mappings for mapped DAC fields of projection DACs must belong to DACs present in the projection query.
  3. The projection query and BQL queries declared in the projection DAC should use the CurrentValue BQL parameter instead of the Current BQL parameter. For FBQL queries you can use the field.FromCurrent.Value FBQL parameter.
  4. You can pass to the projection query only Select queries derived from the PX.Data.SelectBase classes that implement the IBqlSelect<Table> interface. The passed query should be derived from the BqlCommand class.

    You can't pass PXSelectBase derived types to the projection attribute! It will cause runtime error!
     
  5. If projection DAC field specifies multiple BqlField and BqlTable mappings via different attributes declared on it, then DAC fields and DACs specified in these mappings should all be the same.

    This is a very rare scenario that was encountered only a couple of times in our experience. The general recommendation is to avoid such situations.

Unfortunately, these rules are not checked by Acuminator currently. But we have tasks in the Acuminator backlog to add them.

Persist Data with Projection DACs

 

By default, projection DACs are read-only. This means that any change made to them won't be saved to the database. However, you can make a projection DAC editable by setting the Persistent property of PXProjectionAttribute to true. The attribute uses the Select BQL command passed to it (the projection query) to determine which tables should be updated.

However, only the first table referenced by the Select command is updated by default. If the data should be committed not only to the main table, but also to the joined tables, the fields that connect the tables must be marked with PXExtraKeyAttribute, as shown in the following example:

aPXProjection(typeof(
Select2<ContractWatcher,
RightJoin<Contact,
On<Contact.contactID, Equal<ContractWatcher.contactID>>>>),
Persistent = true)] //make the ContractWatcher projection DAC updatable by default
public class SelContractWatcher : ContractWatcher
{
...
#region ContactContactID
public abstract class contactContactID : PX.Data.BQL.BqlInt.Field<contactContactID> {}

// mark that Contact DAC will be updated too on update of SelContractWatcher projection DAC,
// without this attribute only ContractWatcher DAC will be updatable
bPXExtraKey]
[PXDBInt(BqlField = typeof(Contact.contactID))]
public virtual int? ContactContactID { get; set; }
#endregion
...
}

SelContractWatcher DAC can be updated somewhere in the graph with the following code:

var watchers = listWatchers.Cast<SelContractWatcher>()
.Select(item => (SelContractWatcher)Watchers.Cache.CreateCopy(item));
...
foreach (SelContractWatcher watcher in watchers)
{
watcher.ContractID = newContract.ContractID;

//ContractWatcher and Contact DACs will be updated
graph.Watchers.Insert(watcher);
}

Additionally, you can use the PXProjection attribute's constructor with two parameters to explicitly provide the list of editable DACs:

public PXProjectionAttribute(Type select, Typeb] persistent)

The persistent parameter should contain DACs referenced by the Select command. This constructor can be used to make editable only a part of all DACs specified in the projection query. The constructor also sets the Persistent property to true. Here is an example:

// Define projection with explicitly specified editable tables
PXProjection(typeof(Select2<
SOLineSplit,
InnerJoin<SOOrderType,
On<SOOrderType.orderType, Equal<SOLineSplit.orderType>>,
InnerJoin<SOOrderTypeOperation,
On<SOOrderTypeOperation.orderType, Equal<SOLineSplit.orderType>,
And<SOOrderTypeOperation.operation, Equal<SOLineSplit.operation>>>>>>),
persistent: new Type ] { typeof(SOLineSplit), typeof(SOOrderType) })] //list of editable DACs
public class SOLineSplit2 : PXBqlTable
{
#region SOOrderType
public abstract class sOOrderType : PX.Data.BQL.BqlString.Field<sOOrderType> {}

gPXDBString(2, IsFixed = true, BqlField = typeof(SOLineSplit.sOOrderType))]
OPXExtraKey] //Enables updates for joined SOOrderType DAC
public virtual string SOOrderType{ get; set; }
#endregion
...
}

Then SOLineSplit2 projection DAC can be updated somewhere in the graph with the following code:

var current2 = shipmentEntry.Caches=typeof(SOLineSplit2)].Current as SOLineSplit2;

if (current2 != null)
{
current2.ShippedQty = 0;

//only SOLineSplit and SOOrderType DACs will be updated
shipmentEntry.Caches typeof(SOLineSplit2)].Update(current2);
}

Typical Use Cases

 

The most popular projection DACs usage is to create a view in the code that will unite several related DACs into one single projection DAC. The projection DAC can be consumed like a regular DAC which significantly reduces the code size, simplifies the logic and increases the code readability. However, projection DACs also have other popular use cases.

Reducing the Number of DAC Fields Returned from Database

Projection DACs are frequently used to reduce the amount of data retrieved from the database. You can define a simple projection DAC based on some existing DAC with a much smaller number of DAC fields.

The following projection DAC is based on APAdjust DAC. However, it returns from the database only two DAC fields instead of approximately 100 DAC fields declared in APAdjust:

lPXProjection(typeof(Select<APAdjust>))]
tPXHidden]
public class APAdjust3 : PXBqlTable
{
#region AdjgDocType
public abstract class adjgDocType : PX.Data.BQL.BqlString.Field<adjgDocType> {}

&PXDBString(3, IsKey = true, IsFixed = true, InputMask = "", BqlField = typeof(APAdjust.adjgDocType))]
dPXUIField(DisplayName = "AdjgDocType", Visibility = PXUIVisibility.Visible, Visible = false)]
public virtual string AdjgDocType { get; set; }
#endregion

#region AdjgRefNbr
public abstract class adjgRefNbr : PX.Data.BQL.BqlString.Field<adjgRefNbr> {}

&PXDBString(15, IsUnicode = true, IsKey = true, BqlField = typeof(APAdjust.adjgRefNbr))]
aPXUIField(DisplayName = "AdjgRefNbr", Visibility = PXUIVisibility.Visible, Visible = false)]
public virtual string AdjgRefNbr { get; set; }
#endregion
}

Declare Filter for DAC Records

Projection DAC sometimes are used to declare a filter on DAC records. Then you can work with the projection DAC as with a separate entity which helps with the business logic understanding:

lPXProjection(typeof(Select<Vendor,
Where<Vendor.payToVendorID, Equal<CurrentValue<Vendor.bAccountID>>>>))]
public class SuppliedByVendor : Vendor { }

Define Projection DAC to Represent Aggregates

Projection DACs can be used not only to unite several DACs in a single entity but also to create an entity that will represent an aggregate calculated on other DAC. There is a good example of this approach with the retrieval of the latest records by fin periods from the GL history DB table.

The GLHistoryByPeriod projection DAC is used to simplify selection and aggregation of proper GLHistory records on various inquiry and processing screens of the General Ledger module. Its main purpose is to close the gaps in GL history records. Such gaps can appear in cases when GL history records do not exist for every financial period defined in the system. To close these gaps, GLHistoryByPeriod projection DAC declares the LastActivityPeriod field which is calculated from every existing FinPeriod. This allows inquiries and reports that produce information for a given financial period to look at the latest available GLHistory record:

lPXProjection(typeof(Select5<GLHistory,
InnerJoin<FinPeriod,
On<FinPeriod.finPeriodID, GreaterEqual<GLHistory.finPeriodID>>>,
Aggregate<
GroupBy<GLHistory.branchID,
GroupBy<GLHistory.ledgerID,
GroupBy<GLHistory.accountID,
GroupBy<GLHistory.subID,
Max<GLHistory.finPeriodID, // Note the aggregation by MAX
GroupBy<FinPeriod.finPeriodID>>>>>>>>))]
tPXPrimaryGraph(typeof(AccountByPeriodEnq), Filter = typeof(AccountByPeriodFilter))]
public class GLHistoryByPeriod : PXBqlTable
{
// Simplified list of projection DAC fields with their mappingsfor brevity
BranchID //BqlField = typeof(GLHistory.branchID)
LedgerID //BqlField = typeof(GLHistory.ledgerID)
AccountID //BqlField = typeof(GLHistory.accountID)
SubID //BqlField = typeof(GLHistory.subID)

// LastActivityPeriod will contain the calculated value Max<GLHistory.finPeriodID>
LastActivityPeriod //BqlField = typeof(GLHistory.finPeriodID)
FinPeriodID //BqlField = typeof(FinPeriod.finPeriodID)
}

PXProjection Attribute Override

 

Sometimes, you may wish to override a projection attribute of the existing projection DAC. This can be achieved with the usage of a graph extension. Let's see how you can override the projection attribute with an example. Here is the MyProjection projection DAC which uses some BQL query:

cPXProjection(typeof(Select<...Old Projection Query...>))]
public class MyProjection: PXBqlTable
{
#region DocID
public abstract class docID: PX.Data.BQL.BqlInt.Field<docID> { }

tPXDBInt(IsKey = true, BqlField = typeof(SomeDAC.docID))]
public virtual int? DocID { get; set; }
#endregion
...
}

I haven't specified any concrete BQL query because the projection query can be arbitrary and is irrelevant to the subject. I will just call it the old projection query. I have also specified a single projection DAC field mapped to the SomeDAC.docID field. It will be used to demonstrate how you can change mappings of projection DAC fields in a graph extension.

Our goal is to override this query with the new projection query and change all necessary mappings of the projection DAC fields. After the changes MyProjection DAC should behave as if it was declared like this:

cPXProjection(typeof(Select<...New Projection Query...>))]
public class MyProjection: PXBqlTable
{
#region DocID
public abstract class docID: PX.Data.BQL.BqlInt.Field<docID> { }

IPXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public virtual int? DocID { get; set; }
#endregion
...
}

Notice that the DocID field's mapping has changed from SomeDAC.docID field to AnotherDAC.uniqueID field.

To implement the required changes, we must first find all graphs using MyProjection. For simplicity, let's assume that MyProjection DAC is used only by the MyGraph graph:

public class MyGraph : PXGraph<MyGraph>
{
public PXSelect<MyProjection> MyView;
}

To override the projection attribute, we need to declare a graph extension MyGraphProjectionOverrideExt. The overriding logic should be placed inside the Initialize method. The mapping of a projection DAC field is changed by the cache attached event handler declared for the field with a new set of attributes:

public class MyGraphProjectionOverrideExt : PXGraphExtension<MyGraph>
{
// Overriding the projection query by directly modifying the PXCache graph cache
// that corresponds to MyProjection DAC
public override void Initialize()
{
var cache = Base.Caches<MyProjection>();
var child = cache.Interceptor.Child;
Type newProjectionQueryType = typeof(Select<...New Projection Query...>);
cache.Interceptor = new PXProjectionAttribute(newProjectionQueryType);
cache.BqlSelect = cache.Interceptor.GetTableCommand();

if (child != null)
cache.Interceptor.Child = child;
}

// Overriding the DocID field's mapping.
iPXMergeAttributes(Method = MergeMethod.Merge)]
ePXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }
}

The graph extension above will override the projection query and projection DAC fields' mappings for MyProjection DAC but only for the MyGraph graph because the MyGraphProjectionOverrideExt graph extension is applied only to MyGraph.
If there are other graphs using MyProjection DAC, then they will use the old projection query.

In theory, you can declare a graph extension on the base PXGraph type like this:

public class MyGraphExtensionForAllGraphs : PXGraphExtension<PXGraph> {}

This way, the graph extension should be applied to all graphs. However, such approach will affect the whole system and will consume system resources unreasonably. Usually, only a few graphs in the system use a particular projection DAC, and the rest of the graphs will only have a redundant graph extension that does nothing. This is why Acumatica discourages graph extensions declared for all graphs. You should declare graph extensions for graphs that use MyProjection DAC instead.

Reuse Projection Overriding Logic with Generic Graph Extension

It can be tedious to create a separate graph extension for every graph that uses MyProjection DAC, if there are multiple such graphs. There will be a lot of code duplication. Fortunately, you can avoid it by putting all logic into a generic graph extension that will serve as a base type for graph extensions extending concrete graphs:

public abstract class ProjectionOverrideExt<TGraph> : PXGraphExtension<TGraph>
where TGraph : PXGraph, new()
{
// Overriding the projection query by directly modifying the PXCache graph cache
// that corresponds to MyProjection DAC
public override void Initialize()
{
var cache = Base.Caches<MyProjection>();
var child = cache.Interceptor.Child;
Type newProjectionQueryType = typeof(Select<...New Projection Query...>);
cache.Interceptor = new PXProjectionAttribute(newProjectionQueryType);
cache.BqlSelect = cache.Interceptor.GetTableCommand();

if (child != null)
cache.Interceptor.Child = child;
}

// Overriding the DocID field's mapping.
iPXMergeAttributes(Method = MergeMethod.Merge)]
ePXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }
}

Notice that the ProjectionOverrideExt is abstract and has a generic TGraph parameter. Now the support for concrete graphs can be easily added by creating derived graph extensions that substitute TGraph with the concrete graph type:

​public class MyGraphProjectionOverrideExt : ProjectionOverrideExt<MyGraph> { }

public class MyAnotherGraphProjectionOverrideExt : ProjectionOverrideExt<MyAnotherGraph> { }

Customizing DAC Field Attributes in Cache Attached

I would like to delve a little more into the approach used for changing attributes on the DocID DAC field:

oPXMergeAttributes(Method = MergeMethod.Merge)]
PXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }

In the code above the PXMergeAttributes attribute with the MergeMethod.Merge mode is used to tell the runtime that during the calculation of the resulting attributes set it should prefer attributes declared on the cache attached event handler over the attributes declared on the DAC field. During the graph initialization the runtime will use the PXDBInt attribute with a new mapping over the PXDBInt attribute declared on MyProjection DAC.

Quite frequently, developers use a more naïve and concise approach without PXMergeAttributes:

cPXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }

This code looks shorter and simpler, but it's not what you need most of the time because this code will replace all attributes declared on the DAC field with the attributes declared on the cache attached event handler. This opens a way to the code consistency issues, when somebody adds a new attribute to the DAC field later but forgets to add this new attribute to the attributes declared on the cache attached event. Therefore, the graph with the cache attached event handler will never use the new attribute, because it will be removed from the DAC field.

Customizing only the required attribute is a much safer approach that avoids this problem. The changes are applied only to the attribute we wish to modify. If someone adds a new attribute to the DAC field later, the new attribute will be successfully used by the graph with the cache attached event handler.

There are many ways to customize DAC field's attributes in the cache attached event handler with a different level of control. You can read about them here:

Projection DACs and XML Doc Comments

 

It is good practice to write XML doc comments for public API. It is especially helpful for DACs and DAC properties. The Acumatica DAC Schema Browser mechanism even uses XML doc comments to generate descriptions for DACs and DAC fields. If you follow this practice and document your DACs then there is extra advice for the documentation of projection DAC fields.

As you already know, projection DACs can have three types of fields. Unbound DAC fields and DAC fields with PXDBScalar and PXDBCalced attributes are documented as regular DAC fields. There is nothing unusual about them. But mapped DAC fields are different. In most cases, the mapped DAC field of the projection DAC represents the original DAC field and has the same data, behavior and usages. Therefore, it should have the same documentation. The naïve approach would be to just copy the XML doc comment from the original DAC field:

public class OriginalDAC : PXBqlTable
{
#region DacField
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }

/// <summary>
/// DAC field holding important data.
/// </summary>
PXDBString(150, IsUnicode = true)]
public virtual string DacField{ get; set; }
#endregion
}
...
PXProjection(typeof(Select<OriginalDAC>))]
public class ProjectionDAC : PXBqlTable
{
#region DacField
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }

/// <summary>
/// DAC field holding important data.
/// </summary>
PXDBString(150, IsUnicode = true, BqlField = typeof(OriginalDAC.dacField))]
public virtual string DacField{ get; set; }
#endregion
}

However, this approach is very brittle to changes in the documentation. Imagine the situation when the XML doc comment on the original DAC field should be changed. All copy pasted XML doc comments on mapped projection DAC fields must be changed as well, otherwise the documentation will become inconsistent. As you can see, this is a very tedious and error prone approach.

Fortunately, .Net provides a mechanism for this scenario. With the <inheritdoc> XML tag you can reuse the annotation from the specified API:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags#inheritdoc

You can use <inheritdoc> tag for the annotation of all mapped projection DAC field properties. The inheritdoc tag should specify cref attribute referencing the original DAC field property like this:

rPXProjection(typeof(Select<OriginalDAC>))]
public class ProjectionDAC : PXBqlTable
{
// Reuse the docs from the DacField DAC property to add documentation to the BQL field without copy paste.
/// <inheritdoc cref="DacField"/>
#region DacField
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }

// Reuse the docs from the OriginalDAC's DacField DAC property.
/// <inheritdoc cref="OriginalDAC.DacField"/>
DPXDBString(150, IsUnicode = true, BqlField = typeof(OriginalDAC.dacField))]
public virtual string DacField{ get; set; }
#endregion
}

 

4 replies

Userlevel 4
Badge

​In addition, I want to describe how you can check the XML documentation for projection DACs with Acuminator.

 Acuminator PX1007 Diagnostic

Acuminator includes the PX1007 diagnostic that validates that DACs and DAC field properties have non-empty XML doc comments. The diagnostic provides a special support for projection DACs which is close to the provided recommendations. You can read more about this diagnostic here:
https://github.com/Acumatica/Acuminator/blob/dev/docs/diagnostics/PX1007.md

By default, the PX1007 diagnostic is disabled. This rule is used internally at Acumatica to check all new DACs and DAC field properties, but it was decided that enforcing this rule on external developers would be too strict. You can still enable PX1007 rule in Visual Studio options, in the Acuminator section:

Enable PX1007 diagnostic in VS settings

Userlevel 4
Badge

Additional References

 

During the work on this post, I used multiple information sources. Here are some of them:

Thank you for your attention!

Userlevel 7
Badge

Thank you for sharing this with the community @snikomarov36!

Badge +12

Thank you for making this search engine indexable 😍

Reply