Integration von Dynamics CRM 4.0 in MS Office InfoPath 2007 – Teil 3 Der WebService

Oder ist es „das“ WebService? Im Prinzip egal, solange es all das für uns erledigt, was wir von ihm erwarten! Wir befinden uns in Visual Studio und legen ein neues Projekt – also eigentlich ja eine neue Website – an. Dafür wählen wir das Template ASP.NET Web Service und geben ihm einen schönen Namen wie beispielsweise … DataProvider (das ist dann der Foldername, der WebService steht unter dem Titel Service.asmx zur Verfügung). Visual Studio ist hier so nett und stellt uns eine kleine „Hello World“ Webmethode als Beispiel für unseren WebService zur Verfügung.

Als erstes werden wir unserem Service eine Webreferenz auf den WebService von CRM mitgeben. Das machen wir wie schon für das Word-Beispiel diskutiert und binden auch gleich die CrmServiceUtility.cs Klasse aus dem SDK ein, womit sich ganz einfach einen CRM-Service instanzieren lässt.

Im Unterschied zur Standardvariante, in der wir den Service mit Active Directory Credentials ansprechen, müssen wir uns hier um Impersonisation kümmern. Dieses Konzept erlaubt es uns, CRM mit einem anderen Benutzer als dem der den WebService ausführt (das ist üblicherweise der SYSTEM-Account) anzusprechen. Konkret wollen wir uns mit dem Benutzer zu CRM verbinden, der das InfoPath Formular aufruft. Damit können wir garantieren, dass alle Benutzerrechte aber auch Benutzerbeschränkungen aufrecht bleiben. Im Folgenden ein Ausschnitt aus dem Code mit den entsprechenden Änderungen rot markiert:

public static CrmService GetCrmService( string crmServerUrl, string organizationName, Guid UserId) { // Get the CRM Users appointments // Setup the Authentication Token CrmSdk.CrmAuthenticationToken token = new CrmSdk.CrmAuthenticationToken(); token.OrganizationName = organizationName; token.CallerId = UserId; CrmService service = new CrmService(); [...] return service;

}

 

Entsprechend der neuen Variablen(anzahl) müssen wir auch die Rufe der anderen Methoden an unsere neue getCrmService verändern. Diesen geben wir einfach einen Parameter new Guid() mit. CRM ignoriert in diesem Fall die leere Guid und verwendet den aktuellen WebService-Benutzer. Zuletzt geben wir noch alle Verweise und Methoden auf das Metadata-Service weg, da wir es in diesem Fall schlichtweg nicht brauchen.

Was wir an dieser Stelle haben, ist ein Hello World-WebService mit der Möglichkeit, eine Verbindung zu CRM herzustellen. Wechseln wir also in den Code unserer Service.asmx und referenzieren 1. den CRM Sdk und 2. den Namespace unserer CrmServiceUtility. Letztlich haben wir auch noch eine ganze Reihe anderer Dinge, die wir berücksichtigen müssen, weshalb die Liste der Using-Direktiven am Ende so aussieht:

  using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.Services; using System.Xml; using System.Xml.XPath;   using CrmSdk; using Microsoft.Crm.Sdk.Utility; using Microsoft.VisualStudio.Tools.Applications;

Dann müssen wir unserem Service noch einen eigenen Namespace geben, der tempuri.org bringt uns nicht wirklich weiter. Konkret geben wir den späteren Deployment-Pfad an:

[WebService(Namespace =        https://localhost/DataProvider)]

 

WebService Methoden

So weit, so gut. Jetzt kümmern wir uns um die Methoden. Wir wissen, dass wir für folgende Felder im InfoPath Formular eine Datenquelle brauchen: Die Accounts-Liste, die Projekt-Liste, den aktuellen User und den Projekt-Eigentümer.

Accounts abrufen

Beginnen wir mit der Methode, die uns die Accounts-Daten liefert. Wie wir für das Hello World-Beispiel sehen können, müssen wir die Methoden, die über den WebService zur Verfügung gestellt werden sollen, neben der üblichen Deklaration mit Rückgabewert, Name und Parameter auch mit einem kleinen WebMethod-Header versehen:

[WebMethod]

public XmlNode RetrieveAccounts(Guid userId)

 

Als erste Anweisung im Methoden-Body holen wir uns die Verbindung zu unserem CRM (läuft in unserem Beispiel unter der IP 10.10.10.2 an Port 5555 mit Organisationsnamen MicrosoftCRM), wobei wir hier schon erstmals unsere Impersonisation verwenden, also die Guid des aktuellen Benutzers mit übergeben:

CrmService myService =      CrmServiceUtility.GetCrmService(           https://10.10.10.2:5555, "MicrosoftCRM", userId);

 

 

Dann bauen wir uns ein fetchStatement zusammen, mit dem wir alle Accounts abrufen können, für die auch Projekte bestehen und führen die Abfrage mit der Fetch-Methode unseres CRM-Services aus. Da das Ergebnis als string zurückkommt, weisen wir es auf eine entsprechende Variable zu, die wir am Ende mittels XmlDocument.LoadXml(responseString)in ein XML-Dokument verwandeln. Dieses können wir dann – wie in der Deklaration versprochen – als XmlNode zurückgeben.

TIPP: Wenn es Schwierigkeiten beim Ausführen des FetchXML-Statement gibt, als erstes kontrollieren, ob alle Entitäten- und Attributsnamen klein geschrieben sind! Großschreibung kann hier zu Problemen führen.

Mit dem FetchXML-Builder (https://jamesdowney.net/) lassen sich eigens auf CRM zugeschnittene FetchXML-Statements sehr einfach zusammenbauen, die mit dem Tool auch gleich gegen das CRM System getestet werden können. Damit sollten auch keine Schwierigkeiten wegen Groß-/Kelinschreibung auftreten!

Beim Fetch-Statement, wie wir es hier verwenden, machen wir nichts anderes als IDs und Namen aller accounts abzurufen, sie mit den Projekten zu joinen um nur jene zu erhalten, die Projekte zugeordnet haben und dann alle accounts „distinct“ anzuzeigen – also doppelte zu ignorieren.

string fetchStatement = "<fetch mapping=\"logical\" distinct=\"true\">" +   "<entity name=\"account\">" +     "<attribute name=\"accountid\" />" +     "<attribute name=\"name\" />" +     // Einschränkung auf jene accounts,     // die auch Projekte haben     "<link-entity name=\"new_project\"                   from=\"new_accountid\"     to=\"accountid\"                   link-type=\"inner\"                   visible=\"false\" />" +   "</entity>" + "</fetch>"; string responseString =        myService.Fetch(fetchStatement);

Damit sind wir hier auch schon wieder fertig. Wer’s nicht erwarten kann, die Methode zu testen, der kann das auch gleich über Debugging der Solution machen (dafür muss man natürlich eine gültige Systemuser-Guid zur Hand haben, aber die lässt sich ja schnell mal aus der Datenbank ziehen :)

 

Projekte zu Accounts abrufen

Jetzt wenden wir uns der Methode zu, die uns die Projekte für eine bestimmte Account-ID liefert. Hier übergeben wir neben der Benutzer-Guid auch … ja, klar, die Account-ID!

[WebMethod]

public XmlNode RetrieveProjects(                 Guid accountId, Guid userId)

Die Systematik ist genau dieselbe wie bei den Accounts, nur, dass wir hier eine Filter-Condition im FetchXML-Statement definieren. Außerdem brauchen wir als Attribute neben der ID und dem Namen des Projektes auch dessen Start- und Enddatum – auch das wollen wir ja im InfoPath anzeigen! Unser Fetch-Statement schaut demnach wie folgt aus:

string fetchStatement = "<fetch mapping=\"logical\" >" +    "<entity name=\"new_project\">" +       "<attribute name=\"new_name\" />" +       "<attribute name=\"new_projectid\" />" +       "<attribute name=\"new_projectstart\" />" +       "<attribute name=\"new_projectend\" />" +       "<filter>" +         "<condition " +                               "attribute=\"new_accountid\" " +                "operator=\"eq\" " +                "value=\"" + accountId + "\" />" +       "</filter>" + "</entity>" + "</fetch>"; string responseString =        myService.Fetch(fetchStatement);

 

UserId und Projekteigner holen

Schließlich brauchen wir noch Methoden um die Giud des aktuellen InfoPath Benutzers bzw. des Projekteigners zu holen. Für die Abfrage des Projekteigentümers verwenden wir wieder unser impersonisiertes Service und übergeben neben der UserId auch die Projekt-ID für die der Eigentümer festgestellt werden soll. Das Fetch-Statement schaut dann mit der Filter-Condition so aus:

string fetchStatement = "<fetch mapping=\"logical\" >" +    "<entity name=\"new_project\">" +       "<attribute name=\"ownerid\" />" +       "<filter>" +         "<condition " +                               "attribute=\"new_projectid\" " +                "operator=\"eq\" " +                "value=\"" + projectId + "\" />" +       "</filter>" + "</entity>" + "</fetch>";

 

Bei Abfrage der Guid des InfoPath Benutzers verwenden wir keine Impersonisation – schließlich soll uns die Methode ja erst die Guid des Nutzers liefern. Als Parameter übergeben wir deshalb auch nur den Windows-Benutzernamen inklusive Domäne, den wir in InfoPath halt abfragen müssen. Wie das folgende Schnipserl Code zeigt, finden wir für die SystemUser Entität im CRM ein Attribut DomainName, das für jeden Benutzer aus Domaine\BenutzerName besteht:

string fetchStatement = "<fetch mapping=\"logical\" >" +    "<entity name=\"" + EntityName.systemuser + " ">" +       "<attribute name=\"systemuserid \" />" +       "<filter>" +         "<condition " +                          "attribute=\"domainname\" " +           "operator=\"eq\" " +           "value=\"" + WindowsUserName + "\" />" +       "</filter>" + "</entity>" + "</fetch>";

 

Submit-Methode anlegen

So, damit wären wir hier einmal im Prinzip fertig, was wir als letztes noch im Code machen, ist, eine CrmSubmit-Methode bereitzustellen, deren Body wir einstweilen noch leer lassen, von der wir aber jetzt schon erwarten, dass ihr ein string übergeben wird (konkret wird das unser Formular sein). Im letzten Teil unserer Artikelreihe kehren wir dann an diese Stelle zurück … Wir fügen also schnell noch an:

[WebMethod] public void CrmSubmit(string data) { ; }

 

Deployment

Fertig? Fast! Denn das Deployment des WebService fehlt uns noch: Dafür legen wir im wwwroot unseres IIS ein Unterverezichnis an, in das wir unser WebService veröffentlichen (Build/Publish Website). Da wir in unserem Beispiel keine eigene Web-Applikation für unseren WebService definieren wollen, müssen wir jetzt zwei Dinge tun:

  • Wir löschen die beiden Config-Files aus dem Verzeichnis (es gibt ja eh eine web.config im wwwroot)
  • Und wir kopieren den Inhalt des bin Folders in unserem Verzeichnis in den bin-Folder unter wwwroot. Hier ist natürlich Vorsicht geboten, wenn bereits andere custom Assemblies drinnen liegen!

Schließlich testen wir unseren WebService, indem wir ihn unter https://localhost/Verzeichnisname/Service.asmx aufrufen.

Wer seinen WebService auch Remote testen und aufrufen will, der muss im Abschnitt <system.web> in der web.config der Seite (in unserem Beispiel ist das das root-Verzeichnis des IIS) schließlich noch folgenden Unterpunkt hinzufügen, um die entsprechenden Protokolle für WebService-Rufe zu erlauben:

<webServices>
  <protocols>
    <add name="HttpGet"/>
<add name="HttpPost"/>
<add name="HttpSoap"/>
<add name="HttpSoap12"/>
</protocols>
</webServices>

 

Jetzt aber sind wir fertig – wenigstens wenn wir den WebService erfolgreich getestet haben! Im nächsten Abschnitt widmen wir uns der Einbindung der Datenquellen in unser InfoPath Formular. Wir dürfen gespannt sein :)

___________________________________________________

Alexander Brandstätter, Microsoft Consulting Services