Friday, 31 October 2025

Using a temporary table as a datasource in a form

Dynamics 365 Finance and Operations (F&O) has the ability to utilize temporary tables in code. There are many examples of temporary tables in the standard application. However, sometimes there is a requirement to display data in a temporary table in a form, so that the user can make selections or modify the data in the form before the data is processed.

One example in the standard application is when you post sub ledger documents, e.g. purchase or sales orders. The system presents the user with temporary data before the system posts the data and it becomes final.

Below is a simple example of how to populate a temporary table and display it in a simple form. It simply demonstrates the above-mentioned technique of using a temporary table as a dataSource and does not have any real-world value.

Create a temporary table or use an existing one. Note: The Table Type property of the table must be TempDB. So, if you are using an existing temporary table, make sure the Table Type property is TempDB.

Populate your temporary table with data. Note: The data in your table will be lost when the table buffer goes out of scope.


public class AVTmpTableDataProvider
{
    private AVUpdateDataTmpIdRef tmpIdRef; //added here to keep temp table instance alive while there is an object of this class.

    public AVUpdateDataTmpIdRef populateTmpTableDemo()
    {
        for (int j = 0; j < 100; j++)
        {
            tmpIdRef.Id = j;
            tmpIdRef.Name = guid2Str(newGuid());
            tmpIdRef.insert();
        }

        return tmpIdRef;
    }

}

Create a form and add your temporary table as the dataSource. Set the dataSource property of the grid and add some fields to the grid.

In your form, in the dataSource init method, populate your temporary table and then call the linkPhysicalTableInstance method of the dataSource. Note: You have to call this method after the dataSource has been initialized.


[Form]
public class AVTmpTableDialog extends FormRun
{
    //form class with an instance of our temp table. added here to keep temp table instance alive while the form is open.
    private AVTmpTableDataProvider dataProvider = new AVTmpTableDataProvider();

    [DataSource]
    class AVUpdateDataTmpIdRef
    {
        public void init()
        {
            super();

            //populate temp table in form class and link it to this datasource (after datasource has initialized).
            AVUpdateDataTmpIdRef.linkPhysicalTableInstance(dataProvider.populateTmpTableDemo());
        }

    }
}

Custom service using set based operations

Sometimes you just need a fast way to get specific data from Dynamics 365 Finance (F&O) for real-time processing of F&O data in another application. The data analyst and I were debating the architecture, and exporting the F&O data to a data warehouse was not meeting two of the most important requirements: performance and timeliness (i.e., data that must be up to date and not stale).

The client needs the data from F&O to be up to date and in real time without delays. Since the data output requirement did not involve large datasets, my solution was to use set-based operations to fill a temporary table and to expose the data in a custom service. I am very happy with the performance of this service and definitely recommend using this pattern to everyone out there.

On the highest level, there are basically two methods to insert data into the database: row-based and set-based. Let's briefly discuss both methods so that it's clear why I chose the set-based method.

Set-based operations

With set-based operations, the system creates a single SQL statement and sends it to the SQL Server, even if millions of records are processed in the select part of the statement. This statement selects data from tables and inserts it directly into the target table. It does the selection and insertion at the same time. This is extremely effective, and that makes it so fast.

Microsoft documentation: insert_recordset statement – Copy multiple records directly from one or more tables into another table in one database trip.

Row-based operations

Row-based operations are definitely the most used method in the standard application. They are straightforward and flexible to use, but the drawback is that they're slow. Row-based operations insert each row in a separate database round trip. The system first has to fetch the data, create X++ objects for each row, and then create a separate SQL statement for each X++ object / data row that should be inserted. This whole process adds a lot of overhead and therefore is inherently slow because of its design.

Disclaimer

  • The code below is for demonstration purposes only. Use at your own risk.
  • Don't be a silly Willy and use custom services for large datasets. Use other methods for that.

Okay, enough of all that, let's dive into the code part.

Temporary table

Create a temporary table with the fields that you will need for collecting the data and that will be exposed by the custom service. Set the TableType property to TempDB.


Service class

The code of the custom service class orchestrates data retrieval and prepares the data to be exposed by the custom service framework. When the data is retrieved, it uses a set-based operation, i.e., insert_recordset, which is very fast.

The different steps in the service class are:

  • Retrieve data from tables.
  • Populate the temp table.
  • Enumerate the temp table (for each row in the temp table).
    • Transfer the data from the table to an object.
    • Add the populated object to a list.

The code below is the actual code that I created for the client. It is a real-world working example and has not been simplified, because it would have taken too much time and might contain bugs. I did change the prefixes everywhere in the code below for obvious reasons, and I anonymized some of the dimension field names.

The service has two methods, one to retrieve the balances of ledger accounts for a specified period. The second to retrieve all the transactions for a specified main account. The code below is just the first part because I don't want to make the post longer than needed. The two other helper methods demontrate how to use the set-based operations in X++.


public class AVGenJourAccEntrySvc
{
    [AifCollectionTypeAttribute('return', Types::Class, classStr(AVGenJourAccEntryBalances))]
    public List getBalances(DataAreaId _dataAreaId, AccountingDate _accountingDateFrom, AccountingDate _accountingDateTo)
    {
        AVGenJourAccEntrySvcContract    contract;               //data class for retrieving data.
        AVGenJourAccEntryAggTmp         genJourAccEntryAggTmp;  //temp table where our data is stored.
        AVGenJourAccEntrySvcDP          genJourAccEntrySvcDP;   //logic to retrieve data and populate temp table.
        List                            list = new List(Types::Class); //list of data contracts.

        if (_dataAreaId && _accountingDateFrom && _accountingDateTo)
        {
            contract = new AVGenJourAccEntrySvcContract();

            contract.parmAccountingDateFrom(_accountingDateFrom);
            contract.parmAccountingDateTo(_accountingDateTo);
            contract.parmDataAreaId(_dataAreaId);

            changecompany(contract.parmDataAreaId())
            {
                genJourAccEntrySvcDP = AVGenJourAccEntrySvcDP::construct();
                genJourAccEntryAggTmp = genJourAccEntrySvcDP.populateDataBalances(contract); //retrieve data and populate temp table.

                //get data from temp table
                while select genJourAccEntryAggTmp
                    order by genJourAccEntryAggTmp.MainAccountId,
                             genJourAccEntryAggTmp.MainAccountName,
                             genJourAccEntryAggTmp.Dim1CostCenter,
                             genJourAccEntryAggTmp.Dim2Dept,
                             genJourAccEntryAggTmp.Dim3,
                             genJourAccEntryAggTmp.TransactionCurrencyCode
                {
                    
                    AVGenJourAccEntryBalances balances = AVGenJourAccEntryBalancesenJourAccEntryBalances::construct();
                    balances.initFromAVGenJourAccEntryAggTmp(genJourAccEntryAggTmp); //transfer data from temp table to data contract.

                    list.addEnd(balances); //add data object to list.
                }
            }
        }

        return list;
    }
}

The populateDataBalances method uses two other methods that also use set-based operations. All data collected with this class is done purely with set-based operations, making it extremely fast.


public AVGenJourAccEntryAggTmp populateDataBalances(AVGenJourAccEntrySvcContract _contract)
{
    //1. insert general journal account entry data joining with flattened dimension data.
    this.populateBalanceSheetIncomeStatementMovementsTmp(_contract);

    //2. insert general journal account entry data joining with flattened dimension data filtered by main account and dim3.
    this.populateBalanceSheetClosingsTmp(_contract);

    //3. combine and aggregate result sets.
    insert_recordset genJourAccEntryAggTmp
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )
        select
            Company,
            sum(AccountingCurrencyAmount),
            sum(TransactionCurrencyAmount),
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3
        from genJourAccEntryAggTmpOpenBal
        group by
            Company,
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3;

    insert_recordset genJourAccEntryAggTmp
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )
        select
            Company,
            sum(AccountingCurrencyAmount),
            sum(TransactionCurrencyAmount),
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3
        from genJourAccEntryAggTmpAccDim
        group by
            Company,
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3;

    //4. delete zero amount transactions.
    delete_from genJourAccEntryAggTmp
        where genJourAccEntryAggTmp.AccountingCurrencyAmount == 0;

    return genJourAccEntryAggTmp;
}


//1. insert general journal account entry data joining with flattened dimension data.
private void populateBalanceSheetIncomeStatementMovementsTmp(AVGenJourAccEntrySvcContract _contract)
{
    FiscalCalendarPeriod                        fiscalCalendarPeriod;
    MainAccount                                 mainAccount;
    GeneralJournalEntry                         generalJournalEntry;
    GeneralJournalAccountEntry                  generalJournalAccountEntry;
    DimensionAttributeValueGroup                dimensionAttributeValueGroup;
    DimensionAttributeLevelValue                dimensionAttributeLevelValue;
    DimensionCombinationEntity                  dimensionCombinationEntity;

    insert_recordset genJourAccEntryAggTmpOpenBal
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )

        select SubledgerVoucherDataAreaId from generalJournalEntry

        join AccountingCurrencyAmount, TransactionCurrencyAmount, TransactionCurrencyCode from generalJournalAccountEntry
            where generalJournalAccountEntry.GeneralJournalEntry == generalJournalEntry.RecId

        join MainAccountId, Name from mainAccount
            where generalJournalAccountEntry.MainAccount == mainAccount.RecId
                    
        join Dim1CostCenter, Dim2Dept, Dim3
            from dimensionCombinationEntity
                where generalJournalAccountEntry.LedgerDimension == dimensionCombinationEntity.RecordId
            
        exists join fiscalCalendarPeriod
            where generalJournalEntry.FiscalCalendarPeriod == fiscalCalendarPeriod.RecId &&
            
            generalJournalEntry.Ledger         == Ledger::current()                    &&
            generalJournalEntry.AccountingDate >= _contract.parmAccountingDateFrom()   &&
            generalJournalEntry.AccountingDate <= _contract.parmAccountingDateTo()     &&
            fiscalCalendarPeriod.Type          == FiscalPeriodType::Operating          &&
          ((mainAccount.MainAccountId < #FirstIncomeStatmentMainAccount && dimensionCombinationEntity.Dim3 != '') || mainAccount.MainAccountId >= #FirstIncomeStatmentMainAccount); //balance sheet transactions with movements or income statement transactions.
}


//2. insert general journal account entry data joining with flattened dimension data filtered by main account and dim3.
private void populateBalanceSheetClosingsTmp(AVGenJourAccEntrySvcContract _contract)
{
    DimensionValue                              closingMovementCode = '777';
    MainAccount                                 mainAccount;
    GeneralJournalEntry                         generalJournalEntry;
    GeneralJournalAccountEntry                  generalJournalAccountEntry;

    DimensionAttributeValueGroup                dimensionAttributeValueGroup;
    DimensionAttributeLevelValue                dimensionAttributeLevelValue;
    DimensionCombinationEntity                  dimensionCombinationEntity;

    insert_recordset genJourAccEntryAggTmpAccDim
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )

        select SubledgerVoucherDataAreaId from generalJournalEntry

        join AccountingCurrencyAmount, TransactionCurrencyAmount, TransactionCurrencyCode from generalJournalAccountEntry
            where generalJournalAccountEntry.GeneralJournalEntry == generalJournalEntry.RecId

        join MainAccountId, Name from mainAccount
            where generalJournalAccountEntry.MainAccount == mainAccount.RecId

        join Dim1CostCenter, Dim2Dept, closingMovementCode
            from dimensionCombinationEntity
                where generalJournalAccountEntry.LedgerDimension == dimensionCombinationEntity.RecordId &&

            generalJournalEntry.Ledger          == Ledger::current()                    &&
            generalJournalEntry.AccountingDate	>= _contract.parmAccountingDateFrom()   &&
            generalJournalEntry.AccountingDate  <= _contract.parmAccountingDateTo()     &&
            mainAccount.MainAccountId           < #FirstIncomeStatmentMainAccount; //only balance sheet transactions.
}

Service

In order to expose your custom service, you need to create a service and specify your service class and methods. Give the service an appropriate name.




Service group

Create a custom service group and add your service class to the group.




Monday, 29 September 2025

How to encrypt parameter data with Global::editEncryptedField()

Sometimes there is a requirement to encrypt parameter data like passwords, API tokens or other types of secrets. The question is, how is that done in Dynamics 365 Finance and Operations (F&O) without using third party tools?

Well, it's actually straight forward in F&O. You can perform encryption on a table field by using the standard Global::editEncryptedField() method. The base data type of your table field must be of type container. Any other type won't work.

Important

Each F&O environment has it's own encryption key. So, when moving data from the production environment to UAT, Test or Dev, the enrypted data will be obsolete in the new environment. You will have to manually enter the data again in F&O, or export the encrypted data before moving the database, and import the encrypted data after the database movement. The reason for this is because the data was encrypted using the production environments encryption key. The encrypted data cannot be decrypted with the encrpytion key of another environment, only with the encryption key of the environment where it was encrypted.

Steps

1. Add a field of type container to your table. Set the extended data type to EncryptedField.

2. On your table, add an edit method, see code example below.

3. Add a password field to your form and set the data source to your table and data method to your edit method.

Code example

If you want to have a look at an example in the standard application. Have a look at the SysEmailSMTPPassword table and form.


public edit Password editUserPassword(boolean _set, Password _password)
{
    return Global::editEncryptedField(this, _password, fieldNum(YourTableName, Password), _set);
}

Saturday, 27 September 2025

X++ native types vs objects - the fundamental difference you need to understand

In X++ programming, there are simple variable types and more complex types. Native types are primitive variable types like int, str, real, etc. Simple types can be used to store specific variable values of types such as string, number, integer, and so on.

There are also more advanced types in X++. One of them is classes. A class is a representation of a real-life object, like a customer, vendor, product, bank, person, and so on. It can be anything that requires more than a single value, unlike a simple type. In fact, a class uses simple types to store the data in the object. A class can be instantiated to create an instance or object of that class. The object reference is stored in a variable just like simple types, but there is a fundamental difference between simple types and objects.

The biggest difference between these two types that I want to make clear is that object variables are references to objects. So, two different variables can point to the same object. If you change something in the object using the first variable, those changes will reflect in the second variable, and vice versa. It is key to understand this concept because in X++, this is something that is common in the application.

Code examples

Example 1. Native types

Two native type variables assigned the same value. One is changed but different values are printed to the output.


public static void main(Args _args)
{
    real len1, len2;

    len1 = 1.23;
    len2 = len1;
    len2 = 2;
    
    info(strFmt('len1: %1', len1));
    info(strFmt('len2: %1', len2));
}

// Generated output:
//    len2: 2
//    len1: 1.23

Example 2. Object types

Two variables pointing to the same object, or referencing the same object. I am using tables in this example to make it a bit simpler. Tables are actually classes and you don't have to instantiate them like classes.


public static void main(Args _args)
{
    CustTable custTable1, custTable2;

    custTable1.AccountNum = 'C1001';
    custTable1.CustGroup = 'ABC';
    
    custTable2 = custTable1; // assign object 1 to 2. object 2 is now referencing 1, both are pointing to the object 1.
    
    custTable2.CustGroup = 'XYZ'; // change customer group of object 2, object 1 will also reflect this change because it is pointing to the same object.
    
    info(strFmt('custTable1.AccountNum: %1', custTable1.AccountNum));
    info(strFmt('custTable1.CustGroup: %1', custTable1.CustGroup));
    info(strFmt('custTable2.AccountNum: %1', custTable2.AccountNum));
    info(strFmt('custTable2.CustGroup: %1', custTable2.CustGroup));
}

// Generated output:
//    custTable1.AccountNum: C1001
//    custTable1.CustGroup: XYZ
//    custTable2.AccountNum: C1001
//    custTable2.CustGroup: XYZ

Auto submit workflow using X++

Workflows in Dynamics 365 Finance and Operations (F&O) are used to add approval steps to a business process flow. For example, in the purchase-to-pay flow, if approval is enabled and the workflow is configured, assigning a new or different bank account to a vendor account is subject to approval. This means that in F&O, an approver must approve the new bank account before payments can be made to the vendor's new bank account. I highly recommend this vendor bank approval workflow to all my clients, by the way. So, if a user manually assigns a new or different bank account to a vendor in F&O, the system requires approval by an approver.

In the field, one of my clients has an integration (built by a partner, not me) from an external application where the vendors and bank accounts are created and then sent to F&O. The vendors and vendor bank accounts are imported into F&O using a custom framework. The custom framework in F&O creates the vendors and vendor bank accounts and assigns the vendor bank account to the vendor. After this assignment of the bank account to the vendor account, the vendor account is blocked for payment, and the system requires approval via workflow.

Because of the sheer volume that is processed, it's not feasible for someone to manually submit all those new vendors and vendor bank accounts to the workflow. It's not possible to submit them in bulk; they have to be submitted individually.

So, how do we solve this without increased risk? By creating a small customization using X++ and the SysOperation framework. The customization uses the X++ Workflow::activateFromWorkflowType method and automatically submits the vendor and vendor bank accounts to the approval workflow.

Note: The records are only submitted to the workflow, not approved. The approval part of the flow remains and has to be done by an authorized workflow approver in F&O. In this instance, we can identify the source of the data as "external," so we filter the data so that only the externally created data is handled by this customization. The approval is still manual not automatic due to the risk.

To conclude, data can be submitted to the workflow using the Workflow::activateFromWorkflowType method and approvals should not be automated because of the risk.

Code

The code below is the core of my solution. With the SysOperation framework, I always use a minimum of 3 classes. The code below extends the vendor and vend bank account tables and the methods are in the business logic class. If you need more detailed information about what I mean with "3 classes", have a look at Multi selection on forms with the SysOperation framework.

VendTable extension method


public boolean avCanSubmitToWorkflow()
{
    boolean ret = true;

    if (!this.RecId)
    {
        ret = false;
    }

    if (this.WorkflowState == VendTableChangeProposalWorkflowState::NotSubmitted)
    {
        VendTableChangeProposal changeProposal;
        
        changeProposal.disableCache(true);

        select firstOnly RecId from changeProposal
            where changeProposal.VendTable == this.RecId;

        if (!changeProposal)
        {
            ret = false;
        }
    }
    else
    {
        ret = false;
    }

    return ret;
}

VendBankAccount extension method


public boolean avCanSubmitToWorkflow()
{
    boolean ret = false;

    if (this.WorkflowState == VendBankAccountChangeProposalWorkflowState::NotSubmitted && this.requiresApproval())
    {
        ret = true;
    }

    return ret;
}

Vendor code


private Counter runVendors()
{
    Counter cntr;
    QueryRun queryRun;
    VendTable vendTable, vendTableUpdate;

    queryRun = new QueryRun(this.buildQueryVendTable());

    while (queryRun.next())
    {
        vendTable = queryRun.get(tableNum(VendTable));

        if (vendTable.avCanSubmitToWorkflow())
        {
            if (Workflow::activateFromWorkflowType(workFlowTypeStr(VendTableChangeProposalWorkflow), vendTable.RecId, "Auto submit approve manually", NoYes::No))
            {
                update_recordset vendTableUpdate
                    setting WorkflowState = VendTableChangeProposalWorkflowState::Submitted
                        where vendTableUpdate.RecId == vendTable.RecId  &&
                              vendTableUpdate.WorkflowState == VendTableChangeProposalWorkflowState::NotSubmitted;
                cntr++;
            }
        }
    }

    return cntr;
}

Vendor bank account code


private Counter runVendBankAccounts()
{
    Counter cntr;
    QueryRun queryRun;
    VendBankAccount vendBankAccount, vendBankAccountUpdate;

    queryRun = new QueryRun(this.buildQueryVendBankAcc());

    while (queryRun.next())
    {
        vendBankAccount = queryRun.get(tableNum(VendBankAccount));

        if (vendBankAccount.avCanSubmitToWorkflow())
        {
            if (Workflow::activateFromWorkflowType(workFlowTypeStr(VendBankAccountChangeProposalTemplate), vendBankAccount.RecId, "Auto submit approve manually", NoYes::No))
            {
                update_recordset vendBankAccountUpdate
                    setting WorkflowState = VendBankAccountChangeProposalWorkflowState::Submitted
                        where vendBankAccountUpdate.RecId == vendBankAccount.RecId &&
                              vendBankAccountUpdate.WorkflowState == VendBankAccountChangeProposalWorkflowState::NotSubmitted;
                cntr++;
            }
        }
    }

    return cntr;
}

Saturday, 30 August 2025

Custom menu items licensing caveat

In Dynamics 365 Finance and Operations (F&O) menu items are used to launch forms, operations or processes, reports and more. When a developer creates new form, runnable class or report, a custom menu item is also created to launch that particular new form, runnable class or report.

In the application, menu items have two licensing properties:

  • Maintain User License
  • View User License

When a new custom menu item is created, the default value of the Maintain User License and View User License properties defaults to None. At first glance, this fine and is not changed by most developers. In fact, this is dangerous because but the None value is automatically translated Enterprise or Operations in most cases unless specified otherwise by the underlying AOT elements. So for customisations, it always translates to Enterprise.

The important message here is: on your custom menu items, don't leave the value of the Maintain User License and View User License properties to None, always set the Maintain User License and View User License properties to Universal. Unless your custom menu item is referring to a standard element of course, then you can set it accordingly.

Saturday, 23 August 2025

How to view data in a temporary table using SQL Server Management Studio

Temporary tables are used quite often in Dynamics 365 Finance and Operations (F&O) and you can find them everywhere in the standard code and forms. There are different types of temporary tables but in this article we are using the TempDB type. The main advantage of the TempDB type is that you can join them with regular tables because they physically exist in the database.

Every time a temporary table is instantiated in F&O, an actual table is created in the SQL Server tempdb database. The name of the table is unique each time. When the temporary table buffer in F&O goes out of scope, the data in the temporary table is discarded and the table is automatically deleted at some point.

Sometimes when you are troubleshooting code that's using a temporary table, you need to view the contents of a temporary. This cannot be done easily like a regular table using the F&O table browser or SQL Server Management Studio (SSMS). So this begs the question, how do you actually view the data of a temporary table?

The answer is by using the X++ getPhysicalTableName method of the intantiated temporary table. This retrieves the physical SQL Server table name of the temporary table. The most basic method is to temporarily add a line of code using the info method to display the table name. You can then use the table name to query the database with SSMS and view the data as long as the temporary table buffer is still in scope in F&O.

Warning: don't do this in production! The above is only applicable for dev and sandbox environments. This article is for education purposes and does not explain the best possible way to get the table name for your situation. To make thing a bit more elegant, you could write the table name to a log table (e.g. SysExceptionTable) instead of displaying a message in the infolog and you could do execute the line of code for your user account only. This way users won't notice it's there.

Get the physical SQL table name in code


public class MyTmpTableTest
{
    public static void main(Args _args)
    {
        TmpRecIdFilter tmpRecId; //note: TableType must be TempDB
    
        //add some dummy records to the temp table.
        for (Counter cntr = 1; cntr < 50; cntr++)
        {
            tmpRecId.RefRecId = cntr;
            tmpRecId.insert();
        }

        //show the temp table name. this is the only line of code you need,
        //the rest of the code here is just for this test class.
        info(tmpRecId.getPhysicalTableName());

        //for this test class we use a work around to keep temp table bufffer
        //in scope so that the data can be viewed using SSMS.
        //as soon he main method exists and the temp table buffer is out of
        //scope, the data will be lost in the table.
        Box::infoOnceModal('Table name', 'Table name', tmpRecId.getPhysicalTableName(), curUserId());
    }
}

Viewing the data in SQL Server Management Studio

  • Get the table name.
  • Connect to the F&O database using SSMS.
  • Open a new query with the table name form the first step e.g. "select * from [your_temp_table_name_here]"