Wer kennt es nicht, die Datenmigration nach Dynamics 365 dauert länger als gedacht oder die Schnittstelle zum ERP-System trudelt vor sich hin. Die üblichen Tipps wie Plug-ins und Workflows während des Imports abzuschalten, sind für den Live-Betrieb natürlich nicht geeignet.

Wenn man sämtliche Optimierungen zur Beschleunigung von Dynamics-365-Importen, wie mehrere Import-Server, Multithreading oder das Ändern der Import-Reihenfolge bereits durchgeführt hat, gibt es eine weitere, meist unterschätze Möglichkeit, Dynamics-365-On-Premise-Importe zu beschleunigen.

Gerade im Dynamics-365-Enterprise-Bereich ist es üblich, viel mit Lookup zu arbeiten. Das bedeutet, man verweist auf einen anderen Datensatz und trifft auf Basis des verknüpften Datensatzes Entscheidungen. Das heißt, dass die verknüpften Datensätze bei jedem Plug-in-Aufruf immer wieder erneut abgerufen werden müssen. Meistens ändern sich die verknüpften Datensätze jedoch so gut wie nie.

Typische Datensätze, die sich selten ändern

Über Connection wird im Dynamics 365 beispielsweise abgebildet, wer Kontoinhaber ist. Nur bestimmte Mitarbeiter dürfen Kontoberechtigungen verändern. Im Plug-in gilt es also, Felder der ConnectionRole auszuwerten. Die Felder auf der ConnectionRole ändern sich erfahrungsgemäß jedoch so gut wie nie.

var connectionRole = orgSvc.Retrieve(connection.Record1RoleId.LogicalName, connection.Record1RoleId.Id, new ColumnSet(„name“)).ToEntity<ConnectionRole>();

 

if (connectionRole.Name == „KontoInhaber“)

{

if(!IstDerBenutzerFreigabeAdmin())

{

throw new InvalidPluginExecutionException(„Ihnen fehlt die Berechtigung, Kontoberechtigungen zu ändern!“);

}

}

.Net MemoryCache zur Hilfe

ConnectionRoles sind ein Paradebeispiel für Datensätze, die sich nie oder nur selten ändern aber jedes Mal im Plug-in abgerufen werden. Diese zusätzlichen Service-Calls, wirken sich natürlich negativ auf die Geschwindigkeit von Dynamics-365-Importen aus. Es wäre vorteilhaft, wenn man diese Datensätze kurzfristig zwischenspeichern könnte. Hier kommt das .NET Framework zur Hilfe. Über den .Net MemoryCache steht dem Entwickler ein sehr einfach zu bedienender Cache bereit. Der MemoryCache ist sehr einfach zu verwenden und CacheEinträge können nach einem bestimmten Zeitraum ablaufen.

CacheItemPolicy cacheItemExpiration = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(30) }; //Verfällt nach 30 Minuten

MemoryCache.Default.Add(new CacheItem(cacheKey, connectionRole), cacheItemExpiration); // ConnectionRole dem cache hinzufügen

 

var connectionRole = MemoryCache.Default.GetCacheItem(cacheKey)?.Value as ConnectionRole; //ConnectionRole aus dem Cache lesen

MemoryCache und ConnectionRoles

using System;

using System.Linq;

using System.Runtime.Caching;

using Microsoft.Xrm.Sdk;

using Microsoft.Xrm.Sdk.Query;

 

namespace ad.D365.CacheHelper

{

public class ConnectionRoleCache

{

public ConnectionRole GetConnectionRoleFromCache(IOrganizationService orgSvc, string roleName)

{

var cacheKey = „ConnectionRoleCache_“ + roleName;

var connectionRole = MemoryCache.Default.GetCacheItem(cacheKey)?.Value as ConnectionRole;

 

if (connectionRole != null)

{

return connectionRole;

}

 

connectionRole = GetConnectionRoleByName(orgSvc, roleName);

AddConnectionRoleToCache(cacheKey, connectionRole);

 

return connectionRole;

}

 

private ConnectionRole GetConnectionRoleByName(IOrganizationService orgSvc, string roleName)

{

var queryConnectionRoleByName = new QueryExpression(ConnectionRole.EntityLogicalName)

{

ColumnSet = new ColumnSet(„name“),

Criteria =

{

Conditions =

{

new ConditionExpression(„name“,ConditionOperator.Equal, roleName)

}

}

};

return orgSvc.RetrieveMultiple(queryConnectionRoleByName).Entities.First()?.ToEntity<ConnectionRole>();

}

 

private void AddConnectionRoleToCache(string cacheKey, ConnectionRole connectionRole)

{

CacheItemPolicy cacheItemExpiration = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(30) };

MemoryCache.Default.Add(new CacheItem(cacheKey, connectionRole), cacheItemExpiration);

}

}

}

Die ConnectionRoleCache HelperKlasse entschlackt das Plug-in und effektiv übergibt man nur noch den Namen der ConnectionRole und erhält direkt den entsprechenden Datensatz.

var kontoInhaber = new ConnectionRoleCache().GetConnectionRoleFromCache(service, „KontoInhaber“);

 

if (connection.Record1RoleId.Id == kontoInhaber.Id)

{

Gerade im Dynamics-365-EnterpriseBereich kommt ein Lookup selten allein. Meisten benötigt man noch die Sprache des angemeldeten Benutzers oder ob es wichtig ist zu wissen, ob dem angemeldeten Benutzer eine bestimmte Sicherheitsrolle zugewiesen ist. Diese Abfragen lassen sich natürlich ebenfalls über einen MemoryCache zwischenspeichern. So kann man wie in Beispiel 3 dargestellt Service-Calls sparen. Selbst bei solchen trivialen Beispielen gibt es bereits ein hohes Potential, um unnötige Service-Calls einzusparen. Drei eingesparte Service-Calls sorgen definitiv dafür, dass die Dynamics-365-Importe beschleunigt werden.

In Dynamics 365 Online wird für jede Plug-in-Ausführung ein neuer Sandbox-Prozess gestartet und damit auch ein neuer MemoryCache für die neue AppDomain erstellt. Dadurch ist der MemoryCache immer „leer“. Das heißt, diese Variante des Cachings entfaltet seine volle Wirkung nur im On-Premises-Bereich.

Client-Side Caching in WebResourcen

In Zeiten von schnellen Responsive Apps, kann man keinem Benutzer mehr langsame Dynamics-365-Formulare zumuten. Formulare wachsen im Laufe des Dynamics-365-Projektes stetig an und die eine oder andere selbstgebaute WebResource mit ihren langen Ladezeiten überstrapaziert dann schnell die Geduld des Anwenders. Natürlich lässt sich einfach alles in eingeklappten Tabs verstecken, dann lädt das Formular zwar wieder schnell, aber der Anwender muss unnötige weitere Klicks machen, was letztendlich doch keine Zeitersparnis darstellt.

Die meisten WebResourcen laden mehr oder weniger immer dieselben Daten. Ein Klassiker ist ein RibbonButton, der je nach Sicherheitsrolle ein- oder ausgeblendet wird. Statt jedes Mal die Rollen vom Server abzurufen, wäre es vorteilhaft, wenn man diese Abfragen zwischenspeichern könnte.

LocalStorage zur Hilfe

Jeder Dynamics 365 kompatible Browser kommt mit einem eingebauten BrowserCache, dem LocalStorage. Der LocalStorage lässt sich sehr einfach via JavaScript ansprechen.

localStorage.getItem(cacheKey) // abrufen eines Cache-Eintrags

localStorage.setItem(cacheKey, cacheObject) //setzen eines cache-Eintrags

Im Gegensatz zum .Net MemoryCache, laufen Cache-Einträge jedoch nicht automatisch ab.

Mit einem simplen Helper, den man in jeder Dynamics 365 WebResource verwenden kann, gestaltet sich die Benutzung des LocalStorages sehr einfach. Der folgende Helper fragt im CRM nach, ob der aktuelle Benutzer eine bestimmte Sicherheitsrolle hat. Dabei wird der LocalStorage verwendet und das Ergebnis anschließend für sechs Stunden zwischengespeichert.

hasUserGivenRoleCallBack = function (securityRoles) {

var currentUserRoles = Xrm.Utility.getGlobalContext().userSettings.securityRoles

for (var i = 0; i < securityRoles.length; i++) {

for (var j = 0; j < currentUserRoles.length; i++) {

if (currentUserRoles[j] === securityRoles[i].roleid) {

alert(„User has role assigned“);

return;

}

}

}

alert(„User does not has role assigned“);

}

 

getSecurityRoleByNameFromCache = function (securityRoleName, hasUserGivenRoleCallBack) {

 

var cacheKey = getCacheKey(securityRoleName);

var securityRoleCacheObjectAsJSON = localStorage.getItem(cacheKey);

 

if (securityRoleCacheObjectAsJSON !== null || securityRoleCacheObjectAsJSON !== null) {

 

var securityRoleCacheObject = JSON.parse(securityRoleCacheObjectAsJSON);

if (Date.now() – new Date(securityRoleCacheObject.ItemAddedOn) >= 6 * 60 * 60 * 1000) {//expire after 6 hours

retrieveRecord(securityRoleName, hasUserGivenRoleCallBack);

}

else {

hasUserGivenRoleCallBack(securityRoleCacheObject.securityRoleRecord);

}

}

retrieveRecord(securityRoleName, hasUserGivenRoleCallBack);

}

 

getCacheKey = function (securityRoleName) {

 

var organizationSettings = Xrm.Utility.getGlobalContext().organizationSettings

var orgUniqueName = organizationSettings.uniqueName;

 

var cacheKey = ‚configurationRecordCache‘ + orgUniqueName + securityRoleName;

 

return cacheKey;

}

 

retrieveRecord = function (securityRoleName, hasUserGivenRoleCallBack) {

Xrm.WebApi.retrieveMultipleRecords(„role“, „?$select=name&$filter=name eq ‚“ + securityRoleName + „‚“, 1).then(

 

function success(result) {

 

var securityRoleRecord = result.entities;

if (result.entities.length > 0) {

addRecordToCache(securityRoleRecord, securityRoleName);

}

 

hasUserGivenRoleCallBack(securityRoleRecord);

},

function (error) {

console.log(error.message);

}

);

}

 

addRecordToCache = function (securityRoleRecord, securityRoleName) {

 

var securityRoleCacheObject = {};

 

securityRoleCacheObject.ItemAddedOn = Date.now();

securityRoleCacheObject.securityRoleRecord = securityRoleRecord;

 

var securityRoleCacheObjectAsJSON = JSON.stringify(securityRoleCacheObject);

 

localStorage.setItem(getCacheKey(securityRoleName), securityRoleCacheObjectAsJSON);

}

Natürlich sollte man sich immer überlegen was man cached. Grundsätzlich sollte immer eine zusätzliche Server-seitige Sicherheitsvalidierung implementiert sein. Jeder PowerUser kennt die F12-Browser-Tools und kann damit, unabhängig von Caching, Client-seitige „Sicherheits“-Mechanismen sehr einfach aushebeln.

Zusammenfassung und Ausblick

Gerade im Dynamics-365-Enterprise-Bereich werden sehr viele komplexe WebResourcen eingesetzt. Via LocalStorage Caching lassen sich diese massiv beschleunigen. Meisten werden Metadaten, Konfigurationsdatensätze oder Sicherheitsrollen abgerufen. Dies sind Daten, die sich by Design sehr selten ändern und deshalb prädestiniert für Caching sind. Damit können Unternehmen ihren Anwendern bei jedem Formularaufruf wertvolle Sekunden schenken. Gerade für Callcenter-Agents zählt jede (Milli)Sekunde.

Martin Tölk ist Dynamics 365 Customer Engagement Consultant/Developer bei Adesso, www.adesso.de.