Parsea XML y JSON en MSDyn365FO fácilmente

Hace un tiempo tuve que crear un interfasado entre MSDyn365FO yun sistema externo que devolvía los datos en XML. Decidí usar las clases XML de X++ (XmlDocument, XmlNodeList, XmlElement, etc…) para parsear el XML y acceder a los datos. Estas clases son horrorosas. Sacas el trabajo pero de una forma fea fea. Hay un método mejor para parsear XML o JSON en MSDyn365FO.

.NET al rescate

Hay una funcionalidad en Visual Studio que nos ayudará con esto, pero no está disponible para los proyectos de tipo Unified Oprations. Para usarlo sólo hay que abrir Visual Stuio y crear un proyecto de .NET. Ahora copias un ejemplo de XML que quieras parsear, vas al menú Edit, Paste Special, Paste XML As Classes:

Y con esto tendremos un contrato de datos con todos los elementos necesarios para acceder a todos los nodos del XML usando notación con puntos para accede a los datos! Por ejemplo, para este XML de ejemplo obtendremos este código:

namespace AASXMLHelper
{

    // NOTE: Generated code may require at least .NET Framework 4.5 or .NET Core/Standard 2.0.
    /// <remarks/>
    [System.SerializableAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
    [System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
    public partial class catalog
    {

        private catalogBook[] bookField;

        /// <remarks/>
        [System.Xml.Serialization.XmlElementAttribute("book")]
        public catalogBook[] book
        {
            get
            {
                return this.bookField;
            }
            set
            {
                this.bookField = value;
            }
        }
    }

    /// <remarks/>
    [System.SerializableAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
    public partial class catalogBook
    {

        private string authorField;

        private string titleField;

        private string genreField;

        private decimal priceField;

        private System.DateTime publish_dateField;

        private string descriptionField;

        private string idField;

        /// <remarks/>
        public string author
        {
            get
            {
                return this.authorField;
            }
            set
            {
                this.authorField = value;
            }
        }

        /// <remarks/>
        public string title
        {
            get
            {
                return this.titleField;
            }
            set
            {
                this.titleField = value;
            }
        }

        /// <remarks/>
        public string genre
        {
            get
            {
                return this.genreField;
            }
            set
            {
                this.genreField = value;
            }
        }

        /// <remarks/>
        public decimal price
        {
            get
            {
                return this.priceField;
            }
            set
            {
                this.priceField = value;
            }
        }

        /// <remarks/>
        [System.Xml.Serialization.XmlElementAttribute(DataType = "date")]
        public System.DateTime publish_date
        {
            get
            {
                return this.publish_dateField;
            }
            set
            {
                this.publish_dateField = value;
            }
        }

        /// <remarks/>
        public string description
        {
            get
            {
                return this.descriptionField;
            }
            set
            {
                this.descriptionField = value;
            }
        }

        /// <remarks/>
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string id
        {
            get
            {
                return this.idField;
            }
            set
            {
                this.idField = value;
            }
        }
    }


}

Podemos crear esto en una Class Library de .NET y consumirla desde Finance and Operations. Este es el método más rápido para usar todas las clases y miembros de estas. Seguramente se pueda implementar como clases de Dynamics 365 FnO, pero habrís que crear tantas clases como tipos distintos de nodos haya en el XML. Y el propósito original de esto era parsear el XML más rápido. Yo me quedaría con la librería de .NET.

Todos estos pasos también son válidos para un archivo JSON, copias el JSON, paste special y tenemos todas las clases para acceder a los datos.

Usándolo en Dynamics

Una vez tenemos la librería o las clases creadas en Microsoft Dynamics 365 (no hagáis esto anda) añadimos la referencia al proyecto y (siguiendo el ejemplo) hacemos lo siguiente:

catalog			catalog		= new catalog();
XmlSerializer	        serializer	= new XmlSerializer(catalog.GetType());
TextReader		sr	        = new StringReader(xmlSample);
        
catalog = serializer.Deserialize(sr);

catalogBook[]   books   = catalog.book;
catalogBook     book    = books.GetValue(0);

Declaramos una variable del mismo tipo que el nodo raíz (el principal), catalog en el ejemplo. Creamos un nuevo XmlSerializer usando nuestro tipo y creamos un TextReader a partir del XML como cadena. Finalmente necesitamos deserializar el XML y asignar el resultado al catalog y…

Como se ve, podemos acceder a los datos usando notación de puntos y las clases que se crearon al usar la función del pegado especial.

Con la ayuda de herramientas que no son específicas de la programación en X++ podemos conseguir esto, y, definitivamente, es más rápido que tener que parsear el XML usando las clases Xml de Dynamics.

Gestión de características: crear una característica personalizada

La gestión de características (Feature management) lleva disponible en Microsoft Dynamics 365 for Finance and Operations desde hace un tiempo. Antes de eso las características se activaban con el flighting, ejecutando una consulta en SQL en las máquinas de desarrollo y UAT (y en prod lo hacía el equipo de DSE).

Ahora tenemos una área de trabajo (workspace) muy bonito que muestra todas las características disponibles, y el flighting sigue por aquí también. La principal diferencia es que el flighting se usa para activar características a clientes en concreto, como una preview de una feature.

En cada nuevo PU aparecen nuevas funcionalidades a MSDyn365FO, y en el PU30, publicado recientemente bajo el programa PEAP (Preview Early Access Program), se ha añadido más funcionalidad a la Gestión de características, esta vez es la Feature class, una nueva propiedad para los Menu Items y los Menus:

Esto todavía no está disponible, y si intentas añadir una Feature Class a un menu item aparecerá un warning y no habrá ninguna funcionalidad.

Si leemos la documentación veremos que crear características personalizadas todavía no está disponible, pero si buscamos las clases de características en los metadatos y probamos un poco…

Creando una característica personalizada

Vamos a usar la clase TaxSetupValidationFeature como ejemplo. Esta clase implementa la interfaz IFeatureMetadata, y todas las clases de características usan el patrón Singleton para obtener la instancia! (Es emocionante porque es la primera vez que lo veo en MSDyn365FO).

Los métodos que se implementan incluyen el nombre y descripción, el modelo y algo de configuración. Simplemente copiamos todos los métodos y la variable, lo pegamos en la clase que hemos creado y cambiamos lo que haga falta.

 

using Composition = System.ComponentModel.Composition;

[Composition.ExportAttribute(identifierstr(Dynamics.AX.Application.IFeatureMetadataV0))]
public final class AASTestFeature implements IFeatureMetadataV0
{
    private static AASTestFeature instance;


    private void new()
    {
    }

    private static void TypeNew()
    {
        instance = new AASTestFeature();
    }

    [Hookable(false)]
    public FeatureModuleV0 module()
    {
        return FeatureModuleV0::AccountsReceivable;
    }

    /// <summary>
    /// Obtains the singleton object instance.
    /// </summary>
    /// <returns>The <c>AASTestFeature</c> instance.</returns>
    [Hookable(false)]
    public static AASTestFeature instance()
    {
        return AASTestFeature::instance;
    }

    [Hookable(false)]
    public LabelId label()
    {
        return literalStr("Enable Test Feature");
    }

    [Hookable(false)]
    public LabelId summary()
    {
        return literalStr("Enables 'Say hello' button");
    }

    [Hookable(false)]
    public WebSiteURL learnMoreUrl()
    {
        return "";
    }

    [Hookable(false)]
    public boolean isEnabledByDefault()
    {
        return false;
    }

    [Hookable(false)]
    public boolean canDisable()
    {
        return true;
    }

    public static boolean isEnable()
    {
        return FeatureStateProviderV0::isFeatureEnabled(AASTestFeature::instance());
    }

}

Ahora compilamos la solución y vamos al workspace de gestión de características, pulsamos en comprobar actualizaciones y nuestra característica debería estar en la lista:

Vamos a usar la nueva característica (de una forma un poco tonta). Creamos una extensión de un formulario y en su método init comprobamos si la característica está activa, si lo está mostramos un mensaje:

[ExtensionOf(formStr(CustTable))]
final class CustTableAASFeatureDemo_Extension
{
  void init()
    {
        next init();

        if (FeatureStateProviderV0::isFeatureEnabled(AASTestFeature::instance()))
        {
            info('Hello! The feature is enabled!');
        }
    }

}

Antes de activar la característica comprobamos que no se muestra nada en el formulario:

Todo bien, no hay mensaje.

Volvemos a la gestión de características y la activamos.

Volvemos al formulario extendido (la CustTable en mi ejemplo) y…

Y ahi tenemos el mensaje!

Las características personalizadas están funcionando en el PU30, por menos en máquinas de desarrollo, y quizá en entornos sandbox Tier 2+. Pero no probéis esto en producción hasta que esté disponible oficialmente (aunque tampoco es posible al ser un PEAP)

Esto sólo es una pequeña prueba de las clases que están disponibles ya, veremos nuevas funcionalidades en el PU31 cuando la Feature Class funcione, y como leí en Twitter:

Application Checker: mejores practicas de programación?

Si no has trabajado en un ISV hay muchas posibilidades de que nunca te hayas preocupado de las Best Practices de Dynamics (BP), o quizá sí. Yo no he trabajado para un ISV pero cuando empecé a trabajar con AX me dieron el documento de BP de desarrollo y he intentado seguir la mayoría cuando programo.

Pero las BPs se podían ignorar y no seguirlas sin problema. Esto es por lo que Microsoft va a publicar…

Application Checker

Application Checker es una herraminta que pretende cambiar esto. Va a forzar algunas reglas que nuestro código tendrá que cumplir, de otra forma no va a compilar (y quizá ni se despliegue en los entornos).

Tuvimos un pequeño avance en el pasado MBAS, en la sesión “X++ programming with quality” de Dave Froslie y Peter Villadsen. Desafortunadamente la sesión no se grabó.

App checker usa BaseX, una herramienta de análisis XML, y alimenta a Socratex, que usará Microsoft para auditar la calidad del código. No sé si Socratex será publicado y no recuerdo si se dijo nada durante la charla.

El conjunto de reglas se puede encontrar en elproyecto de GitHub de Application Checker y todavía es WIP. Creo que hay muuuuchas cosas a decidir antes de que esto sea GA, y algunas reglas me preocupan y asustan 😛

Tipos de reglas

Hay distintos tipos de reglas, algunas provocarán errores y otras warnings. Por ejemplo:

ExtensionsWithoutPrefix.xq: esta regla provocará un error evitando que tu código compile. Comprueba que una clase de extensión que acabe en _Extension y el atributo ExtensionOf. En caso de que así sea debe tener un prefijo. Por ej.: si extendemos la clase CustPostInvoice no se puede llamar CustPostInvoice_Extension, necesita un prefijo como CustPostInvoiceAAS_Extension.

SelectForUpdateAbsent.xq: esta regla lanzará un warning. Si hay una cláusula forUpdate en un select y no se llama a doUpdate, update, delete, doDelete o write posteriormente nos lo hará saber.

A día de hoy hay 21 reglas en el proyecto de GitHub. Se puede contribuir al proyecto, o puedes crear tus propias reglas sin mandarlas al proyecto y forzarlas en las máquinas de desarrollo con añadirla a la carpeta local de reglas. Yo crearía una que hiciera que después de un if/while/for/switch sea obligatorio dejar el espacio en blanco, pero eso sólo es cosa de mi TOC al escribir/leer código.

Pruébalo en tu código

Podemos usar Application Checker en nuestras VMs de desarrollo desde, creo, el PU26. Sólo tenemos que instalar JRE y BaseX en la máquina y seleccionar el check al hacer una full build.

Algunos ejemplos

ComplexityIndentationCombined.xq

Esta consulta comprueba la (agarráos a la silla)This query checks the (wait for it…) complejidad ciclomática de los métodos. Lo intentaré explicar… la complejidad ciclomática es una métrica de calidad de software, y es el número de caminos independientes que puede seguir el código. Dependiendo del número de ifs, whiles, switches, etc… el código puede tener distintas salidas por distintos caminos, es es lo que calcula la complejidad.

Tomando este ejemplo, que es muy sencillo para demostrarlo, vemos la gran cantidad de caminos que podría seguir:

class AASAppCheckerDemo
{            
    public void complexMethod()
    {
        int a, b, c, d, e, f, g, h, i;

        if (a)
        {
            if (d)
            {				
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }                
            }
            else if (e)
            {
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }                
            }
            else if(f)
            {                
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }
            }
        }
        else if (b)
        {
            if (d)
            {
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }
            }
            else if (e)
            {
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }                
            }
            else if(f)
            {               
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }                
            }
        }
        else if(c)
        {
            if (d)
            {                
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }                
            }
            else if (e)
            {                
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }                
            }
            else if(f)
            {
                if (d)
                {
                }
                else if (e)
                {
                }
                else if(f)
                {
                }
            }
        }        
    }

}

En App checker el error aparece cuando la complejidad es más de 30. He usado el analizador de complejidad Lizard para calcular la complejidad del método y es de 49.

La regla también comprueba la profundidad de indentación, fallando si es mayor de 2. Al final el propósito de estas reglas es que troceemos métodos largos en partes más pequeñas, lo que también ayudará a que nuestro código sea más extensible, como hizo Microsoft con las clases Data Provider de los informes.

BalancedTtsStatement.xq

Con esta regla tengo sentimientos encontrados. La regla comprueba que los ttsbegin y ttscommit de un método estén dentro del mismo ámbito. Así que esto ya no será posible:

public void ttsCheck()
{
    StagingTable stagingTable;

    try
    {
        while select forupdate stagingTable
            where !stagingTable.Processed
        {
            ttsbegin;                
            boolean ret = this.doThings();

            if (ret)
            {
                stagingTable.Processed = true;
                stagingTable.update();
                ttscommit;
            }
            else
            {
                ttsabort;

                ttsbegin;
                stagingTable.Processed	= false;
                stagingTable.ErrorMsg	= 'An error ocurred, see log.';
                stagingTable.update();
                ttscommit;
            }
        }
    }
    catch (Exception::Error)
    {
        ttsabort;

        ttsbegin;
        stagingTable.Processed = false;
        stagingTable.update();
        ttscommit;
    }
    catch
    {
        ttsabort;

        ttsbegin;
        stagingTable.Processed = false;
        stagingTable.update();
        ttscommit;
    }
}

private boolean doThings()
{
    return true;
}

Imagina que has desarrollado una integración con una aplicación externa que introduce datos en una tabla intermedia y que luego se procesan esos datos de forma secuencial. No quieres que si algo falla haya un throw para que el proceso siga con el siguiente registro, así que llamas a ttsabort, guardas el error y continúas. Si esto no es posible… cómo deberemos hacerlo? Crear un lote que genere una tarea por cada línea a procesar?

Además, los modelos del estándar están llenos de ttscommit dentro de ifs.

RecursiveMethods.xq

Esta regla bloqueará el uso de recursividad en métodos estáticos. No tengo muy claro por qué. Application checker debería ser una forma de promover buenas prácticas, no prohibir algunos patrones. Si alguien mete un método recursivo en producción y no se llega nunca a la condición de salida… hola testeo?

Algunas reflexiones finales

¿Va a forzar esto a programar mejor? Lo dudo mucho, pero seguramente no sea el propósito de App Checker. Durante milenios los humanos hemos encontrado la forma de saltarnos las reglas, leyes y todo tipo de restricciones, y esto no será una excepción.

¿Nos ayudará? ¡Joder sí! Pero la mejor forma de asegurarnos de la calidad del código es promoviendo las buenas prácticas en nuestros equipos, a través de formación interna o revisiones de código. E incluso así si a alguien le importa el código limpio un pepino seguirá escribiendo código terrible. Puede que funcione pero no será bonito.

Finalmente, algunas reglas no me convencen. Como lo de evitar la recursividad o el tema de los tts. Tendremos que esperar a ver qué reglas se lanzan finalmente y cómo se va a implantar Application checker en el ALM de MSDyn365FO, bloqueando (o no) los despliegues de código o si se va a incluir en el proceso de build.

Operaciones «set-based» lentas?

En Microsoft Dynamics 365 for Finance and Operations podemos ejecutar operaciones CRUD en el código de dos formas, operaciones set-based (que podríamos traducir como basadas en conjuntos más o menos) y registro a registro.

La recomendación de Microsoft es usar siempre las operaciones set-based, si es posible, como se puede ver en la sesión Implementation Best Practices for Dynamics 365: Performance best practices for a successful Dynamics 365 Finance and Operations implementation del pasado Business Applications Summit de junio.

¿Por qué?

Set-based Vs. Registro a registro

Cuando ejecutamos una query en MSDyn365FO usando la capa de acceso a datos lo que ocurre es que nuestra consulta termina convertida en un comando SQL. Podemos ver las diferencias usando el método getSQLStatement de la clase xRecord con un generateonly en la query (y un forceliterals para mostrar los valores de los parámetros) para obtener la consulta de SQL. Por ejemplo si ejecutamos el siguiente código:

Obtenemos esta consulta de SQL:

SELECT TOP 1 T1.PAYMTERMID,T1.LINEDISC,T1.TAXWITHHOLDGROUP_TH,T1.PARTYCOUNTRY,T1.ACCOUNTNUM,T1.ACCOUNTSTATEMENT,T1.AFFILIATED_RU,T1.AGENCYLOCATIONCODE,T1.BANKACCOUNT,T1.BANKCENTRALBANKPURPOSECODE,T1.BANKCENTRALBANKPURPOSETEXT,T1.BANKCUSTPAYMIDTABLE,T1.BIRTHCOUNTYCODE_IT,T1.BIRTHPLACE_IT,T1.BLOCKED,T1.CASHDISC,T1.CASHDISCBASEDAYS,T1.CCMNUM_BR,T1.CLEARINGPERIOD,T1.CNAE_BR,T1.CNPJCPFNUM_BR,T1.COMMERCIALREGISTER,T1.COMMERCIALREGISTERINSETNUMBER,T1.COMMERCIALREGISTERSECTION,T1.COMMISSIONGROUP,T1.COMPANYCHAINID,T1.COMPANYIDSIRET,T1.COMPANYNAFCODE,T1.COMPANYTYPE_MX,T1.CONSDAY_JP,T1.CONTACTPERSONID,T1.CREDITCARDADDRESSVERIFICATION,T1.CREDITCARDADDRESSVERIFICATIONLEVEL,T1.CREDITCARDADDRESSVERIFICATIONVOID,T1.CREDITCARDCVC,T1.CREDITMAX,T1.CREDITRATING,T1.CURP_MX,T1.CURRENCY,T1.CUSTCLASSIFICATIONID,T1.CUSTEXCLUDECOLLECTIONFEE,T1.CUSTEXCLUDEINTERESTCHARGES,T1.CUSTFINALUSER_BR,T1.CUSTGROUP,T1.CUSTITEMGROUPID,T1.CUSTTRADINGPARTNERCODE,T1.CUSTWHTCONTRIBUTIONTYPE_BR,T1.DEFAULTDIMENSION,T1.DEFAULTDIRECTDEBITMANDATE,T1.DEFAULTINVENTSTATUSID,T1.DESTINATIONCODEID,T1.DLVMODE,T1.DLVREASON,T1.DLVTERM,T1.EINVOICE,T1.EINVOICEATTACHMENT,T1.EINVOICEEANNUM,T1.ENDDISC,T1.ENTRYCERTIFICATEREQUIRED_W,T1.EXPORTSALES_PL,T1.EXPRESSBILLOFLADING,T1.FACTORINGACCOUNT,T1.FEDERALCOMMENTS,T1.FEDNONFEDINDICATOR,T1.FINECODE_BR,T1.FISCALCODE,T1.FISCALDOCTYPE_PL,T1.FORECASTDMPINCLUDE,T1.FOREIGNRESIDENT_RU,T1.FREIGHTZONE,T1.GENERATEINCOMINGFISCALDOCUMENT_BR,T1.GIROTYPE,T1.GIROTYPEACCOUNTSTATEMENT,T1.GIROTYPECOLLECTIONLETTER,T1.GIROTYPEFREETEXTINVOICE,T1.GIROTYPEINTERESTNOTE,T1.GIROTYPEPROJINVOICE,T1.ICMSCONTRIBUTOR_BR,T1.IDENTIFICATIONNUMBER,T1.IENUM_BR,T1.INCLTAX,T1.INSSCEI_BR,T1.INTBANK_LV,T1.INTERCOMPANYALLOWINDIRECTCREATION,T1.INTERCOMPANYAUTOCREATEORDERS,T1.INTERCOMPANYDIRECTDELIVERY,T1.INTERESTCODE_BR,T1.INVENTLOCATION,T1.INVENTPROFILEID_RU,T1.INVENTPROFILETYPE_RU,T1.INVENTSITEID,T1.INVOICEACCOUNT,T1.INVOICEADDRESS,T1.INVOICEPOSTINGTYPE_RU,T1.IRS1099CINDICATOR,T1.ISRESIDENT_LV,T1.ISSUEOWNENTRYCERTIFICATE_W,T1.ISSUERCOUNTRY_HU,T1.LINEOFBUSINESSID,T1.LVPAYMTRANSCODES,T1.MAINCONTACTWORKER,T1.MANDATORYCREDITLIMIT,T1.MANDATORYVATDATE_PL,T1.MARKUPGROUP,T1.MCRMERGEDPARENT,T1.MCRMERGEDROOT,T1.MULTILINEDISC,T1.NIT_BR,T1.NUMBERSEQUENCEGROUP,T1.ONETIMECUSTOMER,T1.ORDERENTRYDEADLINEGROUPID,T1.ORGID,T1.OURACCOUNTNUM,T1.PACKAGEDEPOSITEXCEMPT_PL,T1.PACKMATERIALFEELICENSENUM,T1.PARTY,T1.PARTYSTATE,T1.PASSPORTNO_HU,T1.PAYMDAYID,T1.PAYMENTREFERENCE_EE,T1.PAYMIDTYPE,T1.PAYMMODE,T1.PAYMSCHED,T1.PAYMSPEC,T1.PDSCUSTREBATEGROUPID,T1.PDSFREIGHTACCRUED,T1.PDSREBATETMAGROUP,T1.PRICEGROUP,T1.RESIDENCEFOREIGNCOUNTRYREGIONID_IT,T1.RFC_MX,T1.SALESCALENDARID,T1.SALESDISTRICTID,T1.SALESGROUP,T1.SALESPOOLID,T1.SEGMENTID,T1.SERVICECODEONDLVADDRESS_BR,T1.STATEINSCRIPTION_MX,T1.STATISTICSGROUP,T1.SUBSEGMENTID,T1.SUFRAMA_BR,T1.SUFRAMANUMBER_BR,T1.SUFRAMAPISCOFINS_BR,T1.SUPPITEMGROUPID,T1.TAXGROUP,T1.TAXLICENSENUM,T1.TAXPERIODPAYMENTCODE_PL,T1.TAXWITHHOLDCALCULATE_IN,T1.TAXWITHHOLDCALCULATE_TH,T1.UNITEDVATINVOICE_LT,T1.USECASHDISC,T1.USEPURCHREQUEST,T1.VATNUM,T1.VENDACCOUNT,T1.WEBSALESORDERDISPLAY,T1.AUTHORITYOFFICE_IT,T1.EINVOICEREGISTER_IT,T1.FOREIGNERID_BR,T1.PRESENCETYPE_BR,T1.TAXGSTRELIEFGROUPHEADING_MY,T1.FOREIGNTAXREGISTRATION_MX,T1.CUSTWRITEOFFREFRECID,T1.ISEXTERNALLYMAINTAINED,T1.SATPAYMMETHOD_MX,T1.SATPURPOSE_MX,T1.CFDIENABLED_MX,T1.FOREIGNTRADE_MX,T1.WORKFLOWSTATE,T1.USEORIGINALDOCUMENTASFACTURE_RU,T1.COLLECTIONLETTERCODE,T1.BLOCKFLOORLIMITUSEINCHANNEL,T1.AXZMODEL182LEGALNATURE,T1.AXZCRMGUID,T1.MODIFIEDDATETIME,T1.MODIFIEDBY,T1.CREATEDDATETIME,T1.RECVERSION,T1.PARTITION,T1.RECID,T1.MEMO 

FROM CUSTTABLE T1 

WHERE (((PARTITION=5637144576) AND (DATAAREAID=N'usmf')) AND (ACCOUNTNUM=N'0001'))

Podemos ver que se seleccionan todos los campos y que los filtros en el where incluyen la cuenta que hemos filtrado (además del DataAreaId y Partition).

Cuando se ejecuta un while select en MSDyn365FO, lo que ocurre en SQL Server es que se ejecuta una consulta por cada vuelta del while. Lo mismo ocurre si se ejecuta un update o delete dentro del bucle. Esto se conoce como operación registro a registro.

Imaginad que queréis actualizar la nota de todos los clientes que sean del grupo de clientes 10. Podríamos hacerlo con un while select de esta manera:

Esto ejecutaría tantas consultas en SQL como clientes del grupo 10 existan, una por cada vuelta del bucle. O podríamos usar operaciones set-based:

Esto ejecutará la actualización de todos los clientes del grupo 10 en una única consulta de SQL Server en vez de una por cliente:

UPDATE CUSTTABLE
SET MEMO = 'Special customer'
WHERE (((PARTITION=5637144576) 
    AND (DATAAREAID=N'usmf')) 
    AND (CUSTGROUP=N'10'))

Existen tres operaciones set-based en MSDyn365FO, update_recordset para actualizar, insert_recordset para crear registros y delete_from para borrarlos de forma masiva. Además de poder hacer inserciones masivas con las listas de tipo RecordSortedList y RecordInsertList.

Ejecutar estos métodos en vez de while selects debería ser por lo general más rápido porque se lanza una sola consulta SQL. Pero…

¿Por qué son lentas mis operaciones set-based?

Existen algunos escenarios documentados en los que las operaciones set-based se convierten en operaciones registro a registro como podemos ver en la siguiente tabla:

DELETE_FROMUPDATE_RECORDSETINSERT_RECORDSETARRAY_INSERTUse … to override
Non-SQL tablesYesYesYesYesNot applicable
Delete actionsYesNoNoNoskipDeleteActions
Database log enabledYesYesYesNoskipDatabaseLog
Overridden methodYesYesYesYesskipDataMethods
Alerts set up for tableYesYesYesNoskipEvents
ValidTimeStateFieldType property not equal to None on a tableYesYesYesYesNot applicable

En el ejemplo, si el método update de la CustTable está sobreescrito (que lo está) la operación del update_recordset se ejecutará como si fuera un while select, actualizando registro a registro.

En el caso del update_recordset esto se puede solucionar llamando al método skipDataMethods antes de ejecutar el update:

Esto hace que no se ejecute el método update (o insert si se hace un insert_recordset), más o menos como si se hiciera un doUpdate en el loop. El resto de los métodos también se pueden saltar con el método correspondiente que aparece en la última columna de la tabla.

Así que, para actualizaciones masivas siempre deberíamos usar operaciones set-based, y activar esto también en las data entities con la propiedad EnableSetBasedSqlOperations.

Y ahora viene otro pero.

¿Debería usar siempre operaciones set-based para actualizaciones masivas?

Bueno, depende de con qué datos estemos trabajando. Hay un post magnífico de Denis Trunin llamado «Blocking in D365FO(and why you shouldn’t always follow MS recommendations)» que explica un ejemplo perfecto de cuándo las operaciones set-based podrían ser perjudiciales.

Como siempre, desarrollar en un ERP es bastante delicado, y escenarios parecidos pueden tener soluciones totalmente diferentes. Simplemente hay que analizar y tomar la decisión.

Actualizar Visual Studio 2019 para #MSDyn365FO

¿Harto de desarrollar en Visual Studio 2015? ¿Te sientes abandonado en el pasado? No te preocupes, ¡es posible usar Visual Studio 2017/2019 para desarrollo de Microsoft Dynamics 365 for Finance & Operations!

¿Cuáles son sus ventajas?

¡Absolutamente ninguna! Visual Studio se quedará sin responder sea cual sea la versión que uses porque la extensión de las developer tools está un poco pasada de moda y es la que en realidad causa los cuelgues.

Sí se añade la posibilidad de usar Live Share, que para sesiones de pantalla compartida es mil veces mejor que Teams. Ey, y que estaremos usando la última versión de VS!

¿Cuesta mucho?

No, tiene cero misterio. Lo primero que hay que hacer es descargar Visual Studio 2019 Professional (o Enterprise pero para D365 no sirve de mucho más) e instalarlo:

Seleccionamos la opción de desarrollo de escritorio .NET y le damos al instalar. Cuando termine abrimos VS y nos conectamos con nuestra cuenta.

El siguiente paso es instalar la extensión de las developer tools de Dynamics. Vamos a la unidad K y en la carpeta DeployablePackages encontraremos unos ZIP que si abrimos veremos que tienen una carpeta DevToolsService/Scripts con la extensión de VS:

Otra alternativa es, por ejemplo, descargar el paquete de un Platform Update que también tiene las dev tools y puede que más actualizadas.

Instalamos la extensión y ya nos aparece la opción de VS2019:

Una vez instalado abrimos VS como administrador y…

Y también…

¡Que no cunda el pánico! La extensión está hecha para VS2015 y usarla en una versión más nueva da algunos avisos, pero solo eso, tenemos las herramientas listas y las podemos usar:

Como decía al principio la extensión de las dev tools es lo que a veces ralentiza o bloquea VS, y Visual Studio 2019 así lo notifica:

Pese a los avisos se puede trabajar con Visual Studio 2019 sin ningún problema. Yo llevo haciéndolo una semana y no he encontrado ningún problema que me haga volver a la 2015.

Preview de las nuevas dev tools

En octubre de 2019 se va a publicar la versión de preview de las herramientas de desarrollo tal y como se vio en el MBAS de Atlanta. Esperemos ver qué novedades trae tanto en versión de VS como mejora del rendimiento de la extensión.

Consumir un servicio SOAP en Dynamics 365 for Finance and Operations con ChannelFactory

Si alguna vez necesitas consumir un servicio SOAP desde Dynamics 365 for Finance and Operations, lo primero que tienes que hacer es pedir a los responsables de ese servicio que creen una versión REST. Si eso no es posible, este post es para ti.

Voy a usar este web service que encontré por ahí en http://www.dneonline.com/calculator.asmx para el ejemplo, es una calculadora con las 4 operaciones básicas de suma, resta, división y multiplicación.

Consumir un servicio SOAP en .NET

Empecemos por lo más básico. ¿Cómo consumimos un servicio SOAP en Visual Studio? Muy fácil. Sólo hay que añadir una referencia de servicio en tu proyecto:

Apuntar al servicio web de tu elección:

Esto va a crear la referencia en Visual Studio:

Una vez hecho esto podemos crear una instancia del cliente del servicio y llamar a sus métodos:

3 + 6 = 9, parece que funciona.

Consumir un servicio SOAP en Dynamics 365 for Finance and Operations

Para consumir un servicio web en FnO crea un nuevo proyecto, haz click derecho en referencias y añade la referencia de servicio:

Emmm… no, no se puede, no hay referencias de servicio.

Consumir un servicio SOAP en Dynamics 365 for Finance and Operations (espero…)

El problema es que en las máquinas de desarrollo de 365 no podemos añadir las referencias de servicio en Visual Studio.

¿Qué dice la documentación sobre esto? Bueno, igual que en AX2012 necesitamos crear una clase en .NET que consumira el servicio web y a través de la que accederemos a él desde 365. Muy bien!

Ya lo tenemos. Una referencia a nuestra librería y una runnable class que hará el trabajo:

A ejecutarlo:

¿Qué?

An exception of type ‘System.InvalidOperationException’ occurred in System.ServiceModel.dll but was not handled in user code

Additional information: Could not find default endpoint element that references contract ‘AASSOAPCalculatorService.CalculatorSoap’ in the ServiceModel client configuration section. This might be because no configuration file was found for your application, or because no endpoint element matching this contract could be found in the client element.

¿Contrato? ¿Qué contrato? No sé nada de un contrato. Nadie me ha dicho nada de un contrato! ¿Qué dice la Wikipedia del SOAP?

Soap is the term for a salt of a fatty acid or for a variety of cleansing and lubricating products produced from such a substance.

Creo que no es ese el SOAP que buscaba… (en inglés era más gracioso 🙁 )

SOAP provides the Messaging Protocol layer of a web services protocol stack for web services. It is an XML-based protocol consisting of three parts:

  • an envelope, which defines the message structure and how to process it

  • a set of encoding rules for expressing instances of application-defined datatypes

  • a convention for representing procedure calls and responses

El sobre es el contrato. Un contrato de datos es un acuerdo entre un servicio y un cliente que descibe qué datos se van a intercambiar. Ese contrato.

Consumir un servicio SOAP en Dynamics 365 for Finance and Operations (esta es la buena)

Si vemos el proyecto que hemos creado para consumir el servicio hay un archivo que se llama app.config:

En este archivo tenemos el endpoint que usa la DLL. Este valor es fijo, y en caso de que tengamos un endpoint de pruebas y uno de producción tendríamos que crear una librería para cada uno de ellos. También vemos el contrato que usa, AASSOAPCalculatorService.CalculatorSoap. Como #MSDyn365FO es un ERP basado en web podríamos solucionar esto añadiendo el nodo de  system.serviceModel en el web.config del servidor, no? (app.config para apps de escritorui, web.config para apps web). Sí, pero sólo podríamos hacerlo en desarrollo porque en producción no tenemos acceso al servidor, y en cuanto los entornos Tier2+ se migren a self-service será imposible ahí también.

¿Qué hacemos entonces? Fácil, ChannelFactory<T> al rescate.! ChannelFactory<T> nos permite crear una instancia de la factory para nuestro contrato del servicio y después crear un canal entre el cliente y el servicio. El cliente sería nuestra clase en D365and y el servicio el endpoint (obviamente).

Hacemos lo siguiente:

El objeto BasicHttpBinding puede ser un BasicHttpsBinding si el servicio web corre sobre HTTPS. El objeto EndpointAddress es la URL del servicio. Instanciamos un contrato del servicio de nuestra clase con el binding y el endpoint y creamos el canal. Ahora podemos llamar al servicio web y sus métodos y…

Funciona! Y lo que es mejor, si hay distintos endpoints con este método podemos parametrizarlos y solo necesitamos una DLL para todos los endpoints!

Pero de verdad, evitad los servicios SOAP, tiremos todos de REST.

Definir un tema para cada empresa (prueba de concepto)

Seguramente a estas alturas a casi todos nos han hecho la pregunta “se puede cambiar el color del tema para que sea el de nuestra empresa/marca?”. Desgraciadamente no se puede, pero lo que podemos hacer es definir un tema para cada empresa.

Esta entrada es una prueba de concepto. Todavía no he conseguido que el tema cambie cuando se cambia de empresa por el desplegable superior.

El estándar

Por defecto cada usuario puede seleccionar el tema que quiera en las opciones de usuario:

User info

Si comprobáis la tabla SysUserInfo tiene un campo enumerado Theme de tipo SysUserInfoTheme. Este enum no es extensible, y esa es una de las razones de no poder cambiar los colores estándar (la otra es que la clase que lo controla no es accesible).

El cliente nos puede pedir que se definan unos colores fijos para distintas compañías. Para asegurarse que los usuarios no confunden empresas o incluso entornos.

Vamos a hacerlo

Para este ejemplo he decidido añadir un campo en las empresas para poder definir qué tema usar en cada una de ellas.

Añadimos un nuevo campo de tipo SysUserInfoTheme en la tabla CompanyInfo:

SysUserInfoTheme

Después añadimos el campo en el formulario OMLegalEntity:

OMLegalEntity

Ya tenemos una lista de los temas disponibles. Vamos a añadir la duncionalidad.

Si hacemos una búsqueda de metadatos por el campo Theme de la SysUserInfo encontraremos que se usa en el método GetThemeDensityForCurrentUser de la clase SysFormUtil. Lo vamos a extender de la siguiente manera:

Al retornar el valor de nuestro campo hacemos que el sistema cargue el tema definido en la tabla CompanyInfo en vez del que ha elegido el usuario. Por ejemplo:

USMF

USMF

THMF

SAMF

Diferentes empresas, diferentes temas!

Ahora sólo me queda encontrar una forma de hacer que esto funcione al cambiar de DataArea. He probado con el lookup form que muestra la lista de empresas pero nada. Alguna idea?

Usando Azure Application Insights con MSDyn365FO

Primero de todo… AVISO: antes de usar esto en un entorno de producción pensadlo bien. Y luego volvedlo a pensar. Y si finalmente decides usarlo, hacedlo con cuidado y cariño.

Por qué este aviso? Bueno, a pesar de que los documentos aseguran que la afectación en el rendimiento del sistema en general es mínima hay que andar con cuidado. Es un ERP. Uno en el que no tenemos acceso al entorno de producción (a no ser que estéis On-Prem) para analizar si hay impacto. Además probablemente Microsoft ya está usandolo para recoger datos de los entornos y mostrarlos en LCS, y desconozco si puede haber interferencias. Un montón de no-lo-ses.

Lo usaría en producción? . Puede ser muy útil en algunos casos.

Y dicho esto, de qué voy a escribir que necesita un aviso? Como dice el título, sobre usar Azure Application Insights en Microsoft Dynamics 365 for Finance and Operations. Este post es consecuencia de uno de los «Has visto esto? Sí, deberíamos probarlo!» entre Juanan (aquí en Twitter, seguidle!) y yo. Y el esto esta vez era este post de Lane Swenka en AX Developer Connection. Así que nada original por aquí 🙂

Azure Application Insights

I spy
Made by Cazapelusas

¿Y qué es Application Insights? Como dice la documentación:

Application Insights is an extensible Application Performance Management (APM) service for web developers on multiple platforms. Use it to monitor your blah web application. It will blah blah detect blaaah anomalies. It blah powerful blahblah tools to bleh blah blih and blah blah blaaaah. It’s blaaaaaaaah.

Mmmm… ved este vídeo mejor:

Hay tanta miseria y tristeza en los primeros 30 segundos…

Monitoreo. Eso hace y para eso es. «Eh, pero LCS ya hace eso!«. Vale, monitoreo extra! A todo el mundo le gusta lo extra, como la pizza, excepto si es piña, claro.

Haciendo que funcione

El primer paso será crear un recurso para Application Insights en nuestra suscripción de Azure. Sobre el precio: los 5 primeros gigas por mes son gratuitos, y los datos se guardan durante 90 días. Más información aquí.

Después necesitamos el código. Me voy a ahorrar los detalles en esta parte porque está perfectamente explicado en el link que he puesto antes (este). Básicamente tienes que crear una DLL para manejar los eventos y mandar la información a AAI y usar esa DLL desde MSDyn365FO. En nuestra versión hemos añadido un método extra para trazas llamado trackTrace. Después solo hay que referenciar la DLL en 365 y ya lo podemos usar.

Qué podemos medir?

Ahora viene la parte interesante (espero). Visitas de páginas, capturar errores (o todos los infologs), ejecuciones de lotes, cambios de valor de campos, y cualquier cosa que podamos extender y desde ahí llamar a nuestra API.

Por ejemplo, podemos extender la clase FormDataUtil del motor de formularios. Esta clase tiene varios métodos que se llaman desde los formularios en acciones de los datasources, como validaciones de writes, deletes, campos, etc… Y también esto:

modifiedField in FormDataUtils

Este método se ejecuta cada vez que se modifica un campo en un formulario. Lo vamos a extender para registrar qué campo se ha modificado, el valor anterior y el nuevo. Así:

Extending modifiedField
Prometo que siempre uso etiquetas!

Y como la llamada a Application Insights también guarda el usuario que ha hecho el cambio de valor, tenemos un nuevo log de la base de datos! Incluso mejor, tenemos un nuevo registro de la base de datos que no afecta al rendimiento porque no se generan datos extra en MSDyn365FO. La única pega es que solo se llamará desde formularios, pero puede ser suficiente para monitorear el uso de formularios y los “yo no he tocado ningún parámetro!” 🙂

Esto es lo que vemos en el explorador de métricas de Azure Application Insights:

Azure Application Insights Custom Event
Qué quieres decir con que he tocado eso!?

Sí, fuiste tú usuario Admin! Uy si soy yo…

Custom events

Todas las métricas de los eventos se muestra  en Azure, y los datos se pueden mostrar en Power BI.

Repito, planead bien lo que queréis monitorear antes de usar esto y testeadlo. Luego testeadlo otra vez, sobretodo en entornos SAT con bases de datos Azure SQL. Tienen un rendimiento distinto a un SQL Server normal y hay que estar seguro.

A disfrutar de los datos!

Generando valores de secuencias numericas desde servicios REST y OData

Una de las opciones para realizar integraciones con MSDyn365FO es usar las data entities con servicios REST a través de OData. Para poder usar las entidades con servicios REST solo tenemos que comprobar que tengan la propiedad IsPublic a Yes:

Entidad Clientes V3

Si no la tienen, y es una entity estándar, tocará duplicarla porque no es posible editar la propiedad en una extensión.

Si realizamos una integración con un sistema externo a través de OData para insertar datos en el ERP, nos podemos encontrar con el problema que al generar el registro sea obligatorio el ID de ese registro, como ocurre con los clientes. Si vemos la propiedad Mandatory del campo CustomerAccount está en Auto, con lo que hereda el valor de la propiedad en la tabla CustTable donde esta a Yes.

En este caso si intentamos crear un cliente sin número de cuenta el servicio va a fallar como podemos ver en esta captura de pantalla de Postman:

Postman fail :(

El error es claro, la cuenta de cliente no puede estar vacía.

Esto no sucede así con la entidad de proveedores. «Pero si la cuenta de proveedor también es obligatoria en la VendTable!» dirá alguno. Sí, lo es, pero en la entidad no lo es:

Vendors V2

Para ver cómo lo soluciona el estándar vamos a ver el initValue de la entidad de proveedores:

Tiene truco!

El método skipNumberSequenceCheck es uno de los métodos de datos de la clase Common, y es de la familia de los skipDataMethods, skipDataSourceValidateWrite, skipAosValidation, etc… El método siempre nos devolverá un false a no ser que le digamos antes lo contrario, pasándole el parámetro true en las llamadas en el código.

El método enableNumberSequenceControlForField de la clase NumberSeqRecordFieldHandler lo que hace es inicializar el valor del campo que le pasamos por parámetro con el next de la secuencia numérica que le digamos. En este caso rellena la cuenta de proveedor con la secuencia que este configurada en los parámetros de proveedores para la cuenta de proveedor (lógico).

Reproduciendo el estándar, vamos a crear una clase de extensión para la data entity y extenderemos el método initValue:

Extensión de código de la entity Customers V3

Con esto podemos volver a probar a generar un nuevo cliente en Postman, borrando el parámetro de cuenta de cliente del cuerpo del mensaje, y…

Cliente creado por servicio REST y OData

Premio! Tenemos un nuevo cliente! Creado desde un sistema externo, usando la secuencia numérica del ERP.

Esto no tiene ningún misterio, sobretodo porque reproduce el comportamiento de las partes del estándar que ya lo hacen. Algo que deberíamos intentar hacer siempre, seguir el estándar. Siempre que podamos claro :), porque aunque se intente que los clientes se adapten al estándar lo máximo posible, todos sabemos que siempre, siempre, siempre habrá que hacer alguna customización (menos mal, que somos devs).

Dynamics 365 for Finance & Operations y Azure DevOps (parte II)

En la primera parte de este post vimos a importancia de Azure DevOps y como configurarlo para MSDyn365FO.

Quiero empezar esta segunda parte con una pequeña pataleta. Como decía en la primera parte, los que llevamos años trabajando con AX nos habíamos acostumbrado a no usar un control de versiones. MSDyn365FO nos ha llevado a un terreno sin explorar, con lo que no es raro que cada equipo haya decidido trabajar de una forma u otra según las experiencias que se hayan ido encontrando. Evidentemente, hay un componente del interés de los miembros de estos equipos para investigar un poco por su cuenta sobre la gestión de código, ramas y metodologías. Muchas veces a base de experimentación y prueba-error, y con las prisas de algunos proyectos esto sale mal, o muy mal. Y aquí he echado de menos un poco de guidance por parte de Microsoft (que igual la hay y me lo he perdido).

A pesar de esta quejita el camino y el aprendizaje ha sido, y creo que lo que viene también será, bastante divertido 😉

Estrategias de branching

Vaya por delante que no soy, ni mucho menos, un experto en gestión del código ni Azure DevOps. Todo lo que viene a continuación es fruto, como comentaba antes, de la experiencia (y las meteduras de pata) de casi 3 años trabajando con MSDyn365Ops. En este artículo de la documentación sobre estrategias de branching hay más información sobre branching y links a artículos del equipo de DevOps. Y en la Biblioteca de herramientas y guías de los DevOps Rangers hay incluso muchísimo más!

La verdad es que me encantaría una sesión de FastTrack sobre esto y, creo que no la hay. EDIT: parece que no lo vi y sí que existe una sesión de FastTrack sobre esto que se llama Developer ALM. Gracias a Dag Calafell (twitter) por la información!

Como vimos en la primera parte cuando desplegamos la máquina de Build se crea la carpeta de Main. Lo normal es que en un proyecto de implantación se desarrolle sobre Main hasta el momento del arranque, y que justo antes del go live se cree una branch (rama) de desarrollo. El árbol de código quedaría así:

Ramas despues de branch

En ese momento los mapeos de las máquinas de desarrollo deben cambiarse para que apunten a esta nueva rama de dev. Esto permitirá seguir desarrollando mejoras o corrigiendo errores y decidir el momento en el que se van a mover a producción haciendo un merge a Main.

Esta estrategia es bastante sencilla y que no provoca muchos quebraderos de cabeza. En mi anterior trabajo en cliente final decidimos usar 3 ramas por peculiaridades de la empresa. Main, Dev y Test con merges de Dev a Test y de Test a Main. Un dolor de cabeza al final, gestionar las 3 ramas, con upgrades de versiones, decenas de changesets pendientes y un partner ISV que no ayudaba mucho era bastante divertido.  ¿Pero, y lo que aprendí? Buf.

En cualquier caso un consejo: intentad evitar que se queden changesets pendientes de mergear durante mucho tiempo. La cantidad de conflictos que aparecen y hay que resolver a mano es directamente proporcional a lo viejo que sea el changeset.

Llegado a este punto no puedo hacer suficiente hincapié en lo de normal de más arriba. Como digo, esto lo escribo basado en mis experiencias. Está claro que no es lo mismo trabajar en un partner de implantación que en un ISV. Un ISV tiene la necesidad de mantener diferentes versiones de su producto y no va a usar una rama Main y otra Dev sinó que puede tener una (o varias) por versión a mantener para dar soporte a todos los clientes (aunque desde la 8.1 y el fin del overlayering ya no es necesario). Para más «ideas» el artículo que he enlazado al principio es perfecto para empezar.

Builds

En la primera parte y en otro post (Builds que no responden en Azure DevOps) expliqué un poco acerca de las builds. Ya vimos que la definición de build que se genera por defecto al desplegar el servidor es como la de la imagen inferior:

Pasos de la definición de build por defecto

Esta build tiene todos los pasos con los que se ha creado activos. Podemos desactivar (o borrar) los pasos que no necesitemos. Por ejemplo, los 3 de testeo si no tenemos tests creados, o la sincronización y despliegue de informes.

Podemos crear nuevas definiciones de build a partir de esta o de 0 (pero es más sencillo y rápido duplicar esta y modificarla) para que se apliquen a otras ramas u otros motivos.

Con la versión 8.1 de MSDyn365FO han desaparecido los hotfixes de código X++, todos son binarios. Esto lo que implica es que en la carpeta Metadata de nuestras ramas ya no van a aparecer los modelos del estándar, solo los nuestros. Hasta la versión 8.0 era muy útil tener una definición de build únicamente para nuestros modelos y otra con todos los modelos. Con esto lo que se consigue es tener un DP en mucho menos tiempo que generándolo para todos los modelos. Si se aplica algún hotfix hay que generar el DP de la rama con todos los modelos, pero si sólo hay código propio no hace falta generar un paquete con todos los modelos.

Y hasta aquí la información desactualizada. A estas alturas todos los proyectos deberían estar en 8.1 o listos para estarlo, que en abril llega One Version!

Otra opción que es bastante útil es que, por ejemplo, podemos crear una nueva definición que lo único que haga es compilar una rama:

Definicion build continua

Esta build no hace nada más, solo compila. Así a priori no parece muy útil pero si activamos la opción de integración continua:

DevOps continuous integration

Después de cada check-in de cada desarrollador se lanzará una build que compilará todo el código y fallará si hay algún error. ¿Claro que no debería fallar no? Porque todos compilamos los proyectos antes de hacer el check-in, ¿verdad?

tysonjaja

Pues por eso y porque las prisas son malas y a veces tenemos que vivir con ellas, esta build puede ser bastante útil. Sobretodo cuando lo tengamos configurado para la rama Main y nos «chive» los errores que pueden aparecer después de un merge con conflictos. Y cuando hay que pasar algo urgente a producción y tenemos poco margen nos interesa poder generar el paquete lo antes posible. Usando esta estrategia conseguimos que generar un DP con nuestros paquetes tardara 9 minutos en vez de 1h15m generando todo.

Igual alguien con más conocimiento de esto piensa, pero eso no lo puedes hacer con…

Gated check-ins

Con este tipo de check-in el código se compila ANTES de que el check-in se haga efectivo. Si falla la build el changeset no se hace efectivo hasta que se corrijan los errores y se vuelva a hacer el check-in.

A priori esta opción parece ideal para los check-in de merges de una rama de desarrollo a Main. Los problemas que me he encontrado con esta opción son varios:

  • Si haces múltiples merge y check-ins de un mismo desarrollo y el primero falla no se mergea, pero si el segundo compila correctamente sí.
  • Problemas con las notificaciones de errores y código pendiente al fallar el check-in
  • En merges con más de un check-in se encolan muchas builds (y por defecto solo tenemos un build agent disponible…)

Seguro que esto tiene solución, pero no he sabido encontrarla. Y de todas formas la opción de integración contínua que comentaba antes nos ha funcionado perfectamente para validar que la rama compila sin errores. Como digo todo esto ha sido fruto de la investigación del equipo y prueba-error.

Conclusiones

Supongo que la mayor conclusión es que con MSDyn365FO hay que usar Azure DevOps. Es obligatorio, no hay otra opción. Los que no lo estéis haciendo, si es que alguien no lo hace, hacedlo. Ya. Revisad vuestra forma de trabajo y olvidemos de una vez por todas AX, MSDyn365FO a nivel técnico es otro producto.

La verdad es que, a los desarrolladores, MSDyn365FO nos ha acercado un poco más a lo que es un proyecto de desarrollo de software clásico como puede ser uno de .NET o Java. Pero no del todo. Un proyecto de ERP tiene muchas peculiaridades, y creo que no partir de zero en el desarrollo del producto, tener una base que nos «obliga» un poco a seguir una línea, nos limita en algunos aspectos y en el uso de ciertas técnicas y metodologías.

Y hasta aquí estos dos posts sobre Azure DevOps. Espero que le sean de ayuda a alguien. Y si alguien con más experiencia, o mejores ideas, quiere recomendar algo, ¡los comentarios están abiertos!