Tag Archives: xRM

Query einer Ansicht im Code ausführen

Es kommt häufig vor das eine bestimmte Aktion auf einer Menge Records einer Entität ausgeführt werden soll. Natürlich soll konfiguriert werden können welche Records das sind. Was liegt da näher als die bereits vorhandene “Advance Find” zu verwenden um diesen Filter zu definieren? Continue reading

Build automation und deployment für CRM Solutions mit TFS

In meinem Post “CRM Customizations VS/TFS Integration” habe ich gezeigt wie sich eine CRM Solution in ein Visual Studio Projekt integrieren lässt.
Dafür wird sie entpackt und ihre Komponenten werden auf verschiedene Dateien aufgeteilt. Schlussendlich wird die default Build-Action überschrieben, damit die Solution beim “Build” wieder gepackt wird.

In diesem Post zeige ich, wie die Solution mit einem Buildjob gebuildet und automatisch auf ein CRM System deployt werden kann.
Continue reading

CRM Customizations VS/TFS Integration

Microsoft Dynamics CRM bietet die optimale Grundlage um schnell Projekte abwickeln zu können. Viele Anpassungen an der Applikation erfordern keinen Code. Die Anpassungen werden als „Solutions“, in Form einer ZIP-Datei, exportiert. In Enterprise Szenarien soll jedoch auch für diese Anpassungen die volle ALM Funktionalität von Visual Studio und TFS genutzt werden können.

Continue reading

Project default targets (build, rebuild & clean) überschreiben

Nicht immer möchte man als Output einer Build Action auf einem Project ein Assembly erhalten. Natürlich könnte man einen eigenen Projekttyp erstellen. Dafür muss jedoch jeder Entwickler diese Extension installiert haben.
Es ist viel einfacher die default targets eines CSProj Library Projects zu überschreiben.

Im folgenden Beispiel soll der Build Output eines Projektes ein ZIP File sein. Das Projekt enthält kein Code und muss daher nicht kompiliert werden. Die Ordnerstruktur im Project soll eins zu eins so in das ZIP File übernommen werden. Dabei sollen zwei Build Actions (auf den Items) berücksichtigt werden.
Dabei werden “Content” Items gemäss “Copy to Output Directory” kopiert und in das ZIP gepackt. “Content” Items werden ebenfalls gemäss “Copy to Output Directory” kopiert aber nicht in das ZIP gepackt sondern einfach in das Output Verzeichnis kopiert.
Projektstruktur
Wenn das Projekt keinen Code enthält, muss der Output type “Class Library” sein. Ansonsten wird eine “Main”-Methode verlangt.

Um das zu erreichen, sind drei Schritte notwendig.

  1. Eigenes Targets file erstellen
  2. Default Targets in CSProj löschen
  3. Eigenes Targets file importieren

Eigenes Targets file erstellen
Grundsätzlich lassen sich die Targets direkt im Project-File hinzufügen.
Damit die Targets für mehrer Projects wiederverwendet werden können, ist es ratsam, ein eigenständiges targets file zu erstellen.
Des weiteren ist auch das editieren direkt im Visual Studio möglich (ohne das Projekt schliessen zu müssen).

Es müssen Targets mit den folgenden Namen erstellt werden: “Build”, “Rebuild” und “Clean”. Wobei das “Clean” das Output Verzeichnis leert und “Rebuild” “Clean” und anschliessend “Build” aufruft.

<Target Name="Build">
  <!-- Run the copy for "None" items as usual -->
  SourceFiles="@(None)" Condition="%(None.CopyToOutputDirectory) == 'Always'" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="false" />
  SourceFiles="@(None)" Condition="%(None.CopyToOutputDirectory) == 'PreserveNewest'" DestinationFolder="$(OutputPath)"  SkipUnchangedFiles="true" />

  <!-- The <span class="hiddenSpellError" pre="The " data-mce-bogus="1">Packager</span> needs the exact package structure -->
  <MakeDir Directories="@(Content -> '$(OutputPath)Temp%(RelativeDir)')" />
  SourceFiles="@(Content)" DestinationFolder="$(OutputPath)Temp%(RelativeDir)" />

  <Exec Command="%YOUR_ZIP_TOOL%" />

  <RemoveDir Directories="$(OutputPath)Temp" />
</Target>

Wenn das Project auch über einen TFS Build Agent gebuildet werden soll muss beachtet werden, dass nur Items welche nach “$(OutDir)” kopiert werden auch in den Dropfolder gelangen. Lokal hat “$(OutDir)” denselben Wert wie “$(OutputPath)”.

<PropertyGroup>
  <RebuildDependsOn>
    Clean;
    Build;
  </RebuildDependsOn>
</PropertyGroup>
<Target Name="Rebuild" DependsOnTargets="$(RebuildDependsOn)" />
<Target Name="Clean">
  <RemoveDir Directories="$(OutputPath)" />
</Target>

Default Targets in CSProj löschen
Die folgenden zwei Targets im Project File suchen und löschen.

MSBuildExtensionsPath)$(MSBuildToolsVersion)Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)$(MSBuildToolsVersion)Microsoft.Common.props')" />
MSBuildToolsPath)Microsoft.CSharp.targets" />

Eigenes Targets file importieren
Das erstellte targets File importieren. Wenn es für mehrere Projekt benötigt wird kann es auf Solution Ebene abgelegt und mit einem relativen Pfad referenziert werden.

<Import Project="..MyTargets.targets" />

Shared Projects für Plugins and Workflows

Spätestens wenn man für CRM Online ein Plugin oder eine Custom Workflow Activity entwickelt, kann man keine Assemblies mehr in den GAC oder auf die Disk deployen.
Wie lässt sich nun Code in mehrere Projekte aufteilen oder ein Framework benutzen?

Es gibt drei Optionen.
Option eins und zwei sind aus meiner Sicht nicht empfehlenswert. Der Vollständigkeit halber führe ich sie jedoch auf.

Option 1 Template:
Das CRM Package Project liefert beim erstellen bereits eine Plugin Base Klasse. Das Template kann natürlich angepasst werden.
So hat man mit jedem Projekt sein Framwork mit dabei.
Erstellen von Visual Studio-Vorlagen

Option 2 Linked Items:
Der Code aus einer anderen Library wird als Link in das Projekt eingefügt.
Siehe hier.

Option 3 ILMerge:
Mithilfe von ILMerge lassen sich mehrere Assemblies zu einem mergen.
Davon macht z.B. Log4net gebrauch.

Die Anteilung zeigt wie ILMerge verwendet werden kann.
Dabei habe ich auf die folgenden Kriterien Rücksicht genommen:

  1. Es soll weiterhin das CRM Developer Toolkit verwendet werden könnenn (Rechtsklick –> Deploy).
  2. Debuggen soll weiterhin möglich sein (PDB Merge).
  3. Der Merge muss automatisiert werden.
  4. Die Assemblies müssen mit dem bestehenden SNK signiert werden.
  5. Early-Bound Types müssen weiterhin verwendet werden können.

1. Project-Struktur
Wir haben ein CRM Projekt auf Basis des CRM Developer Toolkits.

ILMerge 1

In diesem Beispiel haben wir vier Projekte. Eines für die Plugins (Plugins), eines für die Workflows (Workflow), eines mit gemeinsam verwendetem Code (Core) und das CrmPackage (CrmPackage). Das CRM Package Project muss das Workflow und das Plugin Project referenzieren. Das Workflow und Plugin Project referenzieren jeweils das Core Project.

2. ILMerge MSBuild Tasks installieren
Das kann ganz einfach über NuGet gemacht werden.

ILMerge 2

3. AfterBuild-Task einfügen
Dem Plugin und dem Workflow Project muss folgender AfterBuild Task hinzugefügt werden.

<UsingTask TaskName="ILMerge.MSBuild.Tasks.ILMerge" AssemblyFile="$(SolutionDir)packagesILMerge.MSBuild.Tasks.1.0.0.3toolsILMerge.MSBuild.Tasks.dll" />
<Target Name="AfterBuild">
<!-- Your configuration goes here -->
<!--- *************************** -->
<!-- Specify the name of the dll files (without file extension) of the referenced assemblies
     which should be merged into the plugin assembly -->
<ItemGroup>
    <ReferencedAssemblyToMerge Include="Core" />
</ItemGroup>
  
<PropertyGroup>
  <SnkFileName>Key.snk</SnkFileName>
</PropertyGroup>

<ItemGroup>
  <!-- The assembly from the current project should also be merged -->
  <MergeAsm Include="$(OutputPath)$(TargetFileName)" />
  <MergeAsm Include="@(ReferencedAssemblyToMerge -> '$(TargetDir)%(identity).dll')" />
</ItemGroup>
  
  <!-- Constants -->
  <!--- *************************** -->
  <PropertyGroup>
    <ILMergeDirectory>ILMerge</ILMergeDirectory>
    <MergedAssembly>$(ProjectDir)$(OutDir)$(ILMergeDirectory)$(TargetFileName)</MergedAssembly>
    <SnkFilePath>..$(SnkFileName)</SnkFilePath>
  </PropertyGroup>
  <ItemGroup>
    <FilesToCopy Include="$(TargetDir)$(ILMergeDirectory)$(TargetName).dll" />
    <FilesToCopy Include="$(TargetDir)$(ILMergeDirectory)$(TargetName).pdb" />
  </ItemGroup>
 
 <!-- Print out informations -->
 <!--- *************************** -->
  <Message Text="ILMerge: Merge '@(MergeAsm)' -&amp;gt; '$(MergedAssembly)'" Importance="high" />
  <Message Text="ILMerge: Sign with '$(SnkFilePath)'" Importance="high" />
  <Message Text="ILMerge: Merge Temp Directory '$(ILMergeDirectory)'" Importance="low" />
  <Message Text="ILMerge: Files to copy from temp '$(FilesToCopy)'" Importance="low" />
  <Message Text="ILMerge: Files to delete after merge '$(FilesToDelete)'" Importance="low" />
  
 <!-- Merge process -->
 <!--- *************************** -->
  <!-- Create temp directory -->
  <MakeDir Directories="$(TargetDir)$(ILMergeDirectory)" />
  <!-- Perform the merge -->
  <ILMerge InputAssemblies="@(MergeAsm)" OutputFile="$(MergedAssembly)" TargetKind="SameAsPrimaryAssembly" SnkFile="$(SnkFilePath)" DebugInfo="True" Closed="True" CopyAttributes="True" />
  <!-- Replace the original file with the merged files -->
  <Copy SourceFiles="@(FilesToCopy)" DestinationFiles="$(TargetDir)%(Filename)%(Extension)" SkipUnchangedFiles="true" />
  <!-- Remove temp directory -->
  <RemoveDir Directories="$(TargetDir)$(ILMergeDirectory)" />
  <!-- Delete the files from the merged projects -->
  <Delete Files="@(ReferencedAssemblyToMerge-> '$(TargetDir)%(identity).dll')" />
  <Delete Files="@(ReferencedAssemblyToMerge-> '$(TargetDir)%(identity).pdb')" />
</Target>

Konfiguration:
MergeAssemblyName: Der Name des Projects welches in das Original-Assembly hinein gemergt werden soll.
SnkFileName: Der Name des Snk-Files, mit dem das gemergte Assembly signiert werden soll.

Erklärung:
Weil weiterhin das CRM Package Project verwendet werden soll, muss das gemergte Assembly, dass Assembly des Projects, (Plugin.dll bzw. Workflow.dll) ersetzen.
Das liesse sich grundsätzlich mit einem einzigen Merge Task erledigen. Will man nun auch das PDB-File mergen (DebugInfo=true) gibt es eine Exception, weil das ursprüngliche PDB-File nicht überschrieben werden kann.
Deshalb wird zuerst ein ein Subdirectory “ILMerge” angelegt und die gemergten Files (DLL und PDB) werden in diesem Ordner abgelegt.
Anschliessend wird das originale DLL und PDB mit den gemergten Files ersetzt und der Ordner gelöscht.
Da nun das Core Assembly zwei Mal im selben Bin Folder liegt, werden z.B. Unit Tests mit der Meldung “ambiguous reference” abbrechen.
Damit das nicht geschieht werden in einem letzten Schritt diese Files gelöscht.

ILMerge Flags:
SnkFile: Damit wird das Assembly signiert. CRM Plugins müssen signiert sein.
CopyAttributes: Damit werden assembly Attribute kopiert. Wenn EarlyBound types verwendet werden, muss das Core Projekt folgende Zeile enthalten: [assembly: Microsoft.Xrm.Sdk.Client.ProxyTypesAssemblyAttribute()]
DebugInfo: Wird hier True gesetzt, wird für das gemergte assembly ein PDB file generiert.

Externe Daten in CRM Grid einbinden

Mithilfe eines RetrieveMultiple Plug-Ins lässt sich die EntityCollection welche als Resultat zurückgegeben wird manipulieren. Das kann nützlich sein um z.B. Entitäten mit Informationen anzureichern oder Daten von einem Fremdsystem direkt im CRM anzuzeigen.

Das Plug-In wird auf der RetrieveMultiple Message registriert. Als Eventing Pipeline Stage wird “Post-Operation” gewählt. Zu diesem Zeitpunkt enthält der Output Parameter “BusinessEntityCollection” bereits alle Entitäten die zurückgegeben werden. Diese Collection lässt sich nun einfach manipulieren. Der folgende Code zeigt, wie eine eigene Dummy Entität hinzugefügt werden kann.

public class Class1 : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

        var outputEntities = context.OutputParameters[&quot;BusinessEntityCollection&quot;] as EntityCollection;

        var entity = new Entity(&quot;account&quot;);
        entity.Attributes.Add(&quot;name&quot;, &quot;test&quot;);
        entity.Attributes.Add(&quot;new_link&quot;, &quot;http://google.ch&quot;);

        outputEntities.Entities.Add(entity);
    }
}

Das nachfolgende Bild zeigt, wie das Plug-In registriert werden muss.

ManipulateEntityCollection

Reliable Import Job für CRM mit Azure und MVC WebAPI

Beim AaaS für Dynamics CRM hat man (verständlicherweise) mit einigen Limitationen zu kämpfen. Das wird gerade bei Enterprise Szenarien schmerzlich bewusst. Wenn zum Beispiel die folgende Anforderung gegeben ist “Jede halbe Stunde soll ein SAP Import laufen. Der SAP Import liefert jeweils viele Daten und die in das CRM zu schreiben dauert min. 10 Minuten. Hier stellt es gleich an zwei Stellen an. Erstens die  infinite loop prevention (siehe Post “Dynamics CRM, Recurring Workflows und die Execution Depth”) und zweitens das Sandbox Timeout. Das Problem lässt sich also nicht oder nur umständlich mit Workflows lösen. Da kommt Azure ins Spiel.

Continue reading

FetchXML und Wildcards

In diesem Post geht es um die Interpretation von Wildcards von Dynamics CRM. Um das Problem zu erklären, muss ich etwas ausholen. Die Anforderung war folgende: Ein CRM Dialog soll die Möglichkeit bieten nach Entitäten zu suchen. Die Suche soll mit “Begins With” funktionieren. Gibt der Benutzer für ein Attribut keinen Wert ein, so soll dieses Attribut nicht berücksichtigt werden. Als Filter ist “and” einzusetzen.

Zum bessern Verständnis hier der gewünschte Dialog:
Wildcard_1

Was hat das nun mit FetchXML und Wildcards zu tun? Beim testen fällt folgendes auf:
Wildcard_3

In Worte gefasst: Sobald ein Attribut, dass in die Suche mit einbezogen wird, keinen Wert enthält, wird der Record nicht mehr zurückgeliefert.

Sehen wir uns also die Query an, die abgesetzt wird:
Wildcard_2

Im oben gezeigten Fall, müsste das somit die folgende sein:

<fetch mapping='logical'>
   <entity name='account'>
      <attribute name = 'name'/>
      <attribute name = 'accountid'/>
      <order attribute='name' descending='false' />
      <filter type='and'>
         <condition attribute = 'name' operator='like' value='Stree%'/>
         <condition attribute = 'name' operator='like' value='%'/>
      </filter>
   </entity>
</fetch>

Nach etwas mehr Testing (mit ausführen der FetchExpression über eine Console Application) kam ich zum Schluss:
Der Wildcard Operator “%” steht für “Any Character” aber nicht für “Empty”.

Diese Erkenntnis war sehr ärgerlich, heisst sie doch, dass sich die Anforderung nicht umsetzen lässt. Oder etwas doch? Mit einem Plug-In auf der “RetrieveMultiple” Message müsste sich die Query manipulieren lassen. Und siehe da, es funktioniert. Hier noch der Code für das Plugin.

public class RetrieveMultiple : IPlugin
{
  public void Execute(IServiceProvider serviceProvider)
  {
    var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

    if (context.InputParameters.Contains("Query") &amp;&amp; context.InputParameters["Query"] is FetchExpression)
    {
      var fetchExpression = (FetchExpression)context.InputParameters["Query"];
      var xDocument = XDocument.Parse(fetchExpression.Query);
      xDocument.Descendants("condition").Where(c => c.Attribute("operator").Value == "like" &amp;&amp; c.Attribute("value").Value == "%").Remove();
      fetchExpression.Query = xDocument.ToString(SaveOptions.DisableFormatting);
    }
  }
}

Bulk Data Load CRM 2013 – Speedtest

Was ist die schnellste Methode um 10’000 neue Accounts in einem CRM Online zu erstellen? Im letzten Post “Bulk Data Load CRM 2013 – Tipps und Tricks” habe ich schon einiges dazu geschrieben. Hier wird es nun etwas konkreter. Continue reading

Bulk Data Load CRM 2013 – Tipps und Tricks

Operationen mit grossen Datenmengen im Microsoft Dynamics CRM sind häufig sehr langsam und daher mühsam. Richtig ärgerlich wird es wenn einem das Sandbox Timeout in einem Plugin oder einer Custom Workflow Activity dazwischen kommt. Continue reading