Megosztás a következőn keresztül:


Rendszerindítás vállalati adatokkal – GYIK

Megjegyzés

  1. novemberi hatállyal:
  • A Common Data Service neve mostantól Microsoft Dataverse. A további tudnivalókat lásd: Power Automate blog.
  • Néhány terminológiát frissítettünk a Microsoft Dataverse szolgáltatásban. Például az entitás mostantól tábla, a mező pedig oszlop. A további tudnivalókat lásd: Terminológia frissítései.

Ezt a témakört hamarosan frissítjük a legújabb terminológia szerint.

Miért van szükség rendszerindításra?

Előfordulhat, hogy üzleti adatokkal rendelkező Dataverse alkalmazáspéldányt vagy más Dynamics 365-alkalmazáspéldányt használ, és engedélyezni szeretné a kettős írású kapcsolatot hozzá. Ebben az esetben a kettős írású kapcsolat engedélyezése előtt rendszerindítást kell végezni a Dataverse- vagy a másik Dynamics 365-alkalmazásadatainál a vállalati információkkal.

Mikor kell használni a rendszerindítást?

A kettős írású táblaleképezések engedélyezése előtt kell rendszerindítást használni (az 5. lépés során).

  1. Ha a Finance and Operations alkalmazás és a Dataverse vagy másik Dynamics 365-alkalmazás példányai között be szeretné állítani a kettős írású kapcsolatot, jelentkezzen be a Finance and Operations alkalmazásba rendszergazdaként.

  2. Lépjen az Adatkezelés modulba, és kattintson a Kettős írás gombra. Ez elindítja az Adatintegrátort.

  3. Hozzon létre kettős írású kapcsolatot legalább egy vállalathoz.

    Kettős írású kapcsolat létrehozása

  4. Engedélyezze a Cdm_companies táblaleképezést. Ez szinkronizálja a vállalatokat a Finance and Operations alkalmazásból a következőbe: Dataverse.

    Táblaleképezés engedélyezése

  5. Futtassa a rendszerindítási mintakódot a Dataverse vagy a másik Dynamics 365-alkalmazás példányában.

  6. Amikor a rendszerindítás után a rendszer készen áll az élő szinkronizálásra, engedélyezze az táblaleképezést.

    Az táblaleképezés engedélyezése elindítja a kezdeti adatszinkronizálási műveletet az engedélyezett táblaleképezésekhez. A program szinkronizálja a kettős írású kapcsolatnál kiválasztott vállalatokra vonatkozó adatokat a Finance and Operations és a Dataverse között.

Hogyan használható a kódminta?

A mintakód egy olyan C#-alkalmazás, amely betölthető a Visual Studio rendszerben. NuGet-csomagfüggőségekre van szükség a Dataverse szoftverfejlesztői készlethez (SDK), ha szokásos Visual Studio-eszközökkel szeretne frissíteni.

Miután kitömörítette és megnyitotta a megoldást a Visual Studio rendszerben, illetve helyreállította a NuGet-csomagokat, keresse meg a TENNIVALÓ részt a kódban. A vállalati adatok rendszerindítására vonatkozó összes döntés esetében TENNIVALÓ jegyzet tartozik, valamint a szabályos megvalósítást bemutató mintakód.

A mintakódban csak a vállalat entitássorainak kategorizálására szolgáló egyik mód jelenik meg. Ha megváltoztatja a logikát a TENNIVALÓ szakaszokban, egyéni kategorizálást hozhat létre.

Mi várható?

Alapértelmezés szerint a mintapályázattal az üzleti egységek és a vállalat közötti kódleképezések tára adható meg. Az OwningBusinessUnit mező használatával rendszerindított entitás automatikusan a megadott vállalatot használja. Az OwningBusinessUnit mező nélküli entitások (pl. egy termék) üres üzletiegység-értékkel való leképezés alapján állítja be a vállalatot.

A konzolpályázat egy paramétert vár: a –szimuláció vagy az –alkalmaz paramétert. Ha a –szimuláció parancssori paramétert használja, akkor semmilyen adat nem frissül. Csak simulation_.csv-fájlok jönnek létre, ugyanabban a könyvtárban, mint amelyikben az eszköz van; minden olyan entitáshoz egy, amelyik frissülne. Ezeket a fájlokat munka közben iteratívan ellenőrizheti, hogy a kód csakugyan a várt módon frissítse a vállalat értékeit.

Ha befejezte a szimulált frissítéseket, használja az –alkalmaz paramétert. Ez frissíti az összes olyan sort, amelyen jelenleg hibás a vállalati érték, egyszerre 1000 soros kötegekben (alapértelmezés szerint). A kód a megadott módon idempotens, vagyis újra futtatható, és csak a helytelenül hozzárendelt vállalatok lesznek frissítve. Az –alkalmaz paraméter futtatásakor a kód az elvégzett változtatások CSV-fájljait adja eredményként; a fájlok neve applied_.csv.

using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Tooling.Connector;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace BootstrapCompany
{
   /// <summary>
   /// Application to bootstrap the company field on existing rows in CDS in preparation for integration to Finance and Operations.
   /// </summary>
   /// <remarks>
   /// This application assumes that the target companies already exist in the CDS environment in the cdm_Company table and are
   /// identified by their company code. It also assumes that the current owning business unit of each row should be used
   /// to categorize by company. This logic can easily be updated to utilize alternate sources of categorization including
   /// custom tables, teams, custom fields on tables, or any other data. This code is provided only as a sample. 
   /// 
   /// To utilize this code, update each of the locations currently denoted with a TODO statement.
   /// 
   /// This code is provided AS IS with no warranties or guarantees, and confers no rights.
   /// </remarks>
   public class Program
   {
       /// <summary>
       /// The number of rows to query and update in CDS in a single operation.
       /// </summary>
       /// <remarks>
       /// The larger this number, the fewer calls will need to be made, so the faster the updates
       /// will complete. However, larger batch sizes are more likely to cause contention. Additionally,
       /// when SQL exceeds some threshold of locks (generally around 5,000), it will escalate to
       /// an entire table lock, which blocks all other activity in the live system on this table. As 
       /// such, a batch size of around 1,000 is relatively fast, while also relatively safe in terms
       /// of contention and transaction time.
       /// </remarks>
       const int requestBatchSize = 1000;

       /// <summary>
       /// The number of faults that may be seen in CDS before the operation is aborted and an exception is thrown.
       /// </summary>
       /// <remarks>
       /// An occassional error due to contention when updating large tables in production is expected, so by default
       /// errors are logged and skipped. However, if a large number of errors are seen, ignoring those errors
       /// in subsequent batches gets expensive, and is usually indicative of a larger issue that should be addressed
       /// before continuing. Faulted requests are *not* retried, but would be picked up in a subsequent run of this script.
       /// </remarks>
       const int maxFaultThreshold = 100;

       /// <summary>
       /// The maximum number of rows per business unit to export when simulating.
       /// </summary>
       /// <remarks>
       /// During simulation, queries are not batched since doing so would require ordering and so be slightly
       /// different from the actual execution logic. To keep this the same between both paths, simulates are
       /// not batched and so a separate maximum number of rows per business unit can be specified.
       /// </remarks>
       const int maxSimulateRecordsPerBusinessUnit = 10000;

       /// <summary>
       /// Whether or not operations should continue if any errors are encountered.
       /// </summary>
       /// <remarks>
       /// This is different than setting maxFaultThreshold = 0, since the first batch of updates will be processed
       /// together. If continueOnError is true and maxFaultThreshold is 0, it is possible that multiple errors may
       /// be encountered and at the same time some rows successfully updated. In a healthy system when updating
       /// a higher number of rows, an occasional spurious error is expected, so it is recommended this be left as true.
       /// </remarks>
       const bool continueOnError = true;

       #region private variables
       private static Dictionary<string, EntityReference> cachedCompanyReferences = new Dictionary<string, EntityReference>();
       #endregion

       /// <summary>
       /// The main execution loop of the program.
       /// </summary>
       /// <param name="args">No arguments are expected.</param>
       static void Main(string[] args)
       {
           if (args.Length != 1 && args[0] != "-simulate" && args[0] != "-apply")
           {
               Console.WriteLine("Usage: BootstrapCompany -simulate");
               Console.WriteLine("       BootstrapCompany -apply");
               Console.WriteLine("The -simulate flag will create a file called simulation.csv in the working");
               Console.WriteLine("directory, but will not change any data. The -apply flag will update live data");
               Console.WriteLine("in the same way that was demonstrated in the simulation.");

               return;
           }

           bool isSimulate = args[0].Equals("-simulate", StringComparison.OrdinalIgnoreCase);

           // Delete the simulation or applied files if existing
           foreach (string existingSimulate in Directory.EnumerateFiles(Directory.GetCurrentDirectory(), $"{(isSimulate ? "simulation" : "applied")}_*.csv"))
           {
               File.Delete(existingSimulate);
           }

           IOrganizationService orgService;

           // TODO: Provide your connection string details for your environment
           CrmServiceClient cdsConnection = new CrmServiceClient("AuthType=Office365;Username=youraliashere@yourdomainhere.com;Password=yourpasswordhere;URL=https://yourorganizationurlhere.crm.dynamics.com/;");
           orgService = (IOrganizationService)cdsConnection.OrganizationWebProxyClient != null ? (IOrganizationService)cdsConnection.OrganizationWebProxyClient : (IOrganizationService)cdsConnection.OrganizationServiceProxy;

           if (orgService != null)
           {
               // Get the current user ID to verify the connection was successful
               Guid userid = ((WhoAmIResponse)orgService.Execute(new WhoAmIRequest())).UserId;

               if (userid != Guid.Empty)
               {
                   Console.WriteLine("Connection Successful!");
               }

               // TODO: Provide a mapping of OwningBusinessUnit name to cdm_Company company ID. You can reuse
               // the same company ID for multiple business units if desired. In this example, it assumes that
               // the business unit named "USMF" is related to the company "USMF". If all rows were owned
               // by the same root business unit, then the first field in the dictionary should be set to the 
               // name of the root business unit, usually the same value as the organization (eg, "Contoso").
               Dictionary<string, string> businessUnitToCompanyMapping = new Dictionary<string, string>()
               {
                   { "", "USMF" }, // The default mapping to use for any entity that doesn't have an owningbusinessunit field
                   { "USMF", "USMF" },
                   { "FRRT", "FRRT" },
               };

               // TODO: Provide a list of tables for which the company field should be backfilled based
               // on owning business unit. The list below represents all existing tables for which a cdm_Company
               // lookup field was added as part of the Finance and Operations dual write project.
               BatchUpdateEntity(orgService, "account", "msdyn_company", businessUnitToCompanyMapping, true, isSimulate, "accountnumber", "name");
               BatchUpdateEntity(orgService, "contact", "msdyn_company", businessUnitToCompanyMapping, true, isSimulate, "fullname");
               // ... Add more here

               // Note, the product entity does not have an owningbusinessunit field like most other tables, so
               // assigning company by Business Unit is not applicable. In this case, whichever mapping specifies an
               // empty business unit will be used to categorize tables without an owningbusinessunit field.
               BatchUpdateEntity(orgService, "product", "msdyn_companyid", businessUnitToCompanyMapping, false, isSimulate, "productnumber");
           }
           else
           {
               Console.WriteLine("Connection failed...");
           }

           Console.WriteLine("Done");
           Console.ReadLine();
       }

       /// <summary>
       /// Updates all incorrectly assigned company relationships for the specified entity.
       /// </summary>
       /// <param name="orgService">The connection to CDS.</param>
       /// <param name="entityName">The logical name of the entity to update.</param>
       /// <param name="companyFieldName">The physical name of the field in the entity being updated which contains the cdm_Company id.</param>
       /// <param name="businessUnitToCompanyMapping">A dictionary of business unit name to company code.</param>
       /// <param name="hasOwningBusinessUnit">true if the entity has an owningbusinessunit field; otherwise, false.</param>
       /// <param name="isSimulate">true to simulate output; otherwise, false.</param>
       /// <param name="fieldsToExport">A set of fields to export into a CSV for this entity if simulating.</param>
       /// <returns>true if the entity was successfully processed without any errors; otherwise, false.</returns>
       private static bool BatchUpdateEntity(
           IOrganizationService orgService, 
           string entityName, 
           string companyFieldName, 
           Dictionary<string, string> businessUnitToCompanyMapping, 
           bool hasOwningBusinessUnit, 
           bool isSimulate, 
           params string[] fieldsToExport)
       {
           List<Guid> faultedIds = new List<Guid>();
           int totalRecordsProcessed = 0;
           Stopwatch stopwatch = new Stopwatch();
           stopwatch.Start();

           string fileName = isSimulate ? "simulation" : "applied";
           StreamWriter simulationWriter = new StreamWriter(Path.Combine(Directory.GetCurrentDirectory(), $"{fileName}_{entityName}.csv"), true);
           simulationWriter.Write("EntityName,EntityId,");
           foreach (string fieldToExport in fieldsToExport)
           {
               simulationWriter.Write($"{fieldToExport},");
           }
           simulationWriter.WriteLine("BusinessUnit,NewCompanyId");

           // Process each mapped business unit individually
           foreach (string businessUnitName in businessUnitToCompanyMapping.Keys)
           {
               Console.WriteLine("Updating any {0} rows for business unit {1} to company {2}...", entityName, businessUnitName, businessUnitToCompanyMapping[businessUnitName]);

               // The empty business unit value is only applicable for tables without an owning business unit field
               if (hasOwningBusinessUnit && string.IsNullOrEmpty(businessUnitName))
               {
                   continue;
               }
               else if (!hasOwningBusinessUnit && !string.IsNullOrEmpty(businessUnitName))
               {
                   continue;
               }

               var companyRef = GetCompanyReference(orgService, businessUnitToCompanyMapping[businessUnitName]);

               // Iteratively loop in batches to keep transaction lock size small
               bool moreRecordsExist = true;

               while (moreRecordsExist)
               {
                   moreRecordsExist = false;

                   // Find the first batch of rows for this business unit with the wrong company ID. Ordering
                   // is not explicity specified, but SQL will most likely process based on the index starting with
                   // company ID, since all new company ID fields added for Finance and Operations integration have
                   // also added a new index starting with company ID. Explicitly specifying order would reduce the
                   // query plan options for SQL and introduce unnecessary overhead.
                   QueryExpression query = new QueryExpression(entityName);
                   query.ColumnSet.AddColumns(companyFieldName);
                   foreach (string fieldToExport in fieldsToExport)
                   {
                       query.ColumnSet.AddColumn(fieldToExport);
                   }
                   query.Criteria.AddCondition(companyFieldName, ConditionOperator.NotEqual, companyRef.Id);

                   // TODO: Uncomment the line below if you only want to fill in companies that are empty
                   // as opposed to the line above which updates the company any time it differs from the 
                   // desired value
                   // query.Criteria.AddCondition(companyFieldName, ConditionOperator.Equal, Guid.Empty);

                   if (isSimulate)
                   {
                       // During simulation, get as a single block of rows to avoid positioning complexities
                       query.TopCount = maxSimulateRecordsPerBusinessUnit;
                   }
                   else
                   {
                       // Only batch rows during actual application, otherwise retrieve all as a single operation
                       query.TopCount = requestBatchSize + faultedIds.Count;
                   }

                   // For tables with an owning business unit, join based on business unit name
                   if (hasOwningBusinessUnit)
                   {
                       // TODO: Replace this logic with different algorithms to determine the correct company
                       // in situations where business unit is not the best way to categorize.
                       LinkEntity linkEntity = query.AddLink("businessunit", "owningbusinessunit", "businessunitid", JoinOperator.Inner);
                       linkEntity.Columns.AddColumns("name");
                       linkEntity.LinkCriteria.AddCondition("name", ConditionOperator.Equal, businessUnitName);
                   }

                   var multipleRequest = new ExecuteMultipleRequest()
                   {
                       Settings = new ExecuteMultipleSettings()
                       {
                           ContinueOnError = true,
                           ReturnResponses = true
                       },
                       Requests = new OrganizationRequestCollection()
                   };

                   EntityCollection result = orgService.RetrieveMultiple(query);

                   int rowsAddedToBatch = 0;

                   foreach (var entity in result.Entities)
                   {
                       // Skip any previously faulted ID's. These values will be re-queried with each batch
                       // which is inefficient, but is more efficient than passing hundreds of ID values to 
                       // the underlying SQL query to be skipped at the database level (assuming the 
                       // max fault count is relatively small).
                       if (faultedIds.Contains(entity.Id))
                       {
                           continue;
                       }

                       entity.Attributes[companyFieldName] = companyRef;
                       
                       UpdateRequest updateRequest = new UpdateRequest()
                       {
                           Target = entity
                       };

                       simulationWriter.Write($"{entityName},{entity.Id},");
                       foreach (string fieldToExport in fieldsToExport)
                       {
                           simulationWriter.Write($"{entity.Attributes[fieldToExport]},");
                       }
                       simulationWriter.WriteLine($"{businessUnitName},{businessUnitToCompanyMapping[businessUnitName]}");

                       // Only add the update request when applying for real
                       if (!isSimulate)
                       {
                           multipleRequest.Requests.Add(updateRequest);
                       }

                       rowsAddedToBatch++;
                       Console.Write(".");
                   }

                   totalRecordsProcessed += rowsAddedToBatch;

                   if (rowsAddedToBatch > 0 && !isSimulate)
                   {
                       Console.Write("Sending {0} updates in a batch", rowsAddedToBatch);
                       var updateResult = orgService.Execute(multipleRequest) as ExecuteMultipleResponse;
                       moreRecordsExist = true;
                       Console.WriteLine(" done");

                       // If any faults are encountered, flag those IDs to not be processed again
                       // in subsequent batches.
                       if (updateResult.IsFaulted)
                       {
                           foreach (var response in updateResult.Responses)
                           {
                               if (response.Fault != null)
                               {
                                   Console.WriteLine(response.Fault);
                                   faultedIds.Add(((UpdateRequest)multipleRequest.Requests[response.RequestIndex]).Target.Id);

                                   if (faultedIds.Count > 100)
                                   {
                                       throw new ApplicationException("Excessive number of update failures, aborting operation");
                                   }
                               }
                           }
                       }
                   }
                   else
                   {
                       Console.WriteLine("No {0} rows remain to be updated for {1}->{2}", entityName, businessUnitName, businessUnitToCompanyMapping[businessUnitName]);
                   }
               }
           }

           simulationWriter.Close();
           simulationWriter = null;

           stopwatch.Stop();
           Console.WriteLine("Processed {0} rows for the {1} entity in {2}ms.", totalRecordsProcessed, entityName, stopwatch.ElapsedMilliseconds);

           return (faultedIds.Count == 0);
       }

       /// <summary>
       /// Gets an entity reference to the company with the specified ID if one exists.
       /// </summary>
       /// <param name="orgService">The CDS connection.</param>
       /// <param name="companyId">The company ID to search for.</param>
       /// <returns>An entity reference if one exists; otherwise, null.</returns>
       private static EntityReference GetCompanyReference(IOrganizationService orgService, string companyId)
       {
           if (cachedCompanyReferences.ContainsKey(companyId))
           {
               return cachedCompanyReferences[companyId];
           }

           QueryExpression query = new QueryExpression("cdm_company");
           query.ColumnSet.AddColumns("cdm_companyid");
           query.Criteria.AddCondition("cdm_companycode", ConditionOperator.Equal, companyId);
           query.TopCount = 1;

           EntityCollection result = orgService.RetrieveMultiple(query);

           EntityReference entityRef = null;

           foreach (var entity in result.Entities)
           {
               entityRef = entity.ToEntityReference();
               break;
           }

           cachedCompanyReferences[companyId] = entityRef;

           return entityRef;
       }
   }
}