Im zweiten Teil
der Blogreihe habe ich euch gezeigt, wie ihr den Client in eure Projekte einbindet und grundlegend verwendet. Abschließend möchte ich euch heute noch mitgeben, wie ihr so manche Besonderheiten der Web API in Hinsicht auf Lookup-Spalten im Client meistern könnt und welche Verbesserungen und Features für weitere Versionen geplant sind.
Früher oder später kommt der Zeitpunkt, an dem ihr mit dem Client auch Datensätze aus Dataverse mit Lookup-Spalten verarbeiten möchtet. Diese unterliegen in der Web API im Gegensatz zu einfachen Spalten allerdings einigen Besonderheiten, die ihr bei der Nutzung des Clients und dem Design eurer Model-Klassen beachten müsst.
Serialisierung und Deserialisierung
Habt ihr mal versucht, das Salesorder-Model aus dem letzten Artikel zu verwenden, um über die Update-Methode des Clients den Auftrag in Dataverse zu aktualisieren?
Falls ja, dann werdet ihr festgestellt haben, dass das so nicht funktioniert. Grund dafür ist, dass nicht alle Spalten, die die Web API zurück gibt, auch wieder 1:1 als Eingabe akzeptiert werden. Die Id-Spalte kann beispielsweise nicht aktualisiert werden, ist im aktuellen Model aber dennoch nach der Serialisierung im JSON enthalten.
Die größten Unterschiede bei der Serialisierung und Deserialisierung gibt es bei Lookup-Spalten. Je nachdem, ob ein Datensatz von der Web API gelesen oder geschrieben wird, müssen diese unterschiedlich verarbeitet werden. Mehr zur allgemeinen Funktionsweise der Web API findet ihr in den Microsoft Docs
.
Um mit dieser Problematik im Client umzugehen, habe ich bisher immer den Lösungsansatz verfolgt, ein Model mit unterschiedlichen Eigenschaften für die Serialisierung und Deserialisierung zu pflegen. Dazu werden die Getter und Setter der Eigenschaften wie im nachfolgenden Beispiel entsprechend angepasst. Den Rest erledigt System.Text.Json für uns.
[DataverseTable("salesorder", "salesorders")]
public class SalesOrder
{
[DataverseRowId]
[JsonIgnore]
public Guid Id { get; set; }
[JsonPropertyName("salesorderid")]
public string __in_id
{
set
{
Id = value;
}
}
/// Die Eigenschaft, die die Lookup-Spalte in der Anwendung
/// repräsentiert und mit welcher innerhalb dieser gearbeitet wird.
[JsonIgnore]
public Guid QuoteId { get; set; }
/// Da der Getter private ist, wird diese Eigenschaft nur bei
/// der Deserialisierung berücksichtigt. Der Wert wird an die
/// QuoteId-Eigenschaft durchgereicht. Diese Eigenschaft existiert
/// nur als Proxy zu QuoteId und sollte in der Anwendung nicht
/// direkt verwendet werden.
[JsonPropertyName("_quoteid_value")]
public string __in_quoteid
{
set
{
QuoteId = new Guid(value);
}
}
/// Da der Setter private ist, wird diese Eigenschaft nur bei
/// der Serialisierung berücksichtigt. Der Wert aus der QuoteId-
/// Eigenschaft wird verarbeitet und entsprechend heraus geschrieben.
/// Diese Eigenschaft existiert nur als Proxy zu QuoteId und sollte
/// in der Anwendung nicht direkt verwendet werden.
[JsonPropertyName("quoteid@odata.bind")]
public string __out_quoteid
{
get
{
if (QuoteId == null || QuoteId == Guid.Empty)
{
return null;
}
// associate
return $"/quotes({QuoteId})";
}
}
}
Lookup-Spalten mit unterschiedlichen Typen
Dem obigen SalesOrder-Model kann entnommen werden, wie wir den Getter einer Eigenschaft im Model so aufbauen können, dass der durch die Guid repräsentierte Datensatz in Dataverse beim Schreiben der Lookup-Spalte verknüpft wird. Das konnten wir deshalb bewerkstelligen, da wir an dieser Stelle sicher wussten, dass sich hinter der Guid ein Angebot verbirgt. Wie aber kann so ein Getter auch für Lookup-Spalten umgesetzt werden, hinter denen sich unterschiedliche Typen verstecken können (z.B. Besitzer, der sowohl User als auch Team sein kann)?
Die Antwort liefert die Microsoft.Dynamics.CRM.lookuplogicalname
-Annotation. Sie wird in der JSON-Response der Web API zur Lookup-Spalte mitgeliefert und verrät uns die Tabelle, die sich hinter der Guid versteckt. Mit Hilfe dieser Annotation kann zur Laufzeit entschieden werden, welche Art von Datensatz sich hinter der Guid verbirgt.
[DataverseTable("salesorder", "salesorders")]
public class SalesOrder
{
...
[JsonIgnore]
public Guid OwnerId { get; set; }
[JsonPropertyName("_ownerid_value")]
public string _in_ownerid
{
set
{
OwnerId = new Guid(value);
}
}
[JsonIgnore]
public string OwnerType { get; set; }
[JsonPropertyName("_ownerid_value@Microsoft.Dynamics.CRM.lookuplogicalname")]
public string _in_ownertype
{
set
{
OwnerType = value;
}
}
[JsonPropertyName("ownerid@odata.bind")]
public string _out_ownerid
{
get
{
if (OwnerId == null || OwnerId == Guid.Empty)
{
return null;
}
return OwnerType switch
{
"team" => $"/teams({OwnerId})",
"systemuser" => $"/systemusers({OwnerId})",
_ => null
};
return null;
}
}
}
Ausblick
Aktuell umfasst die Definition der Getter und Setter für Lookup-Spalten in den Model-Klassen recht viel Boilerplate-Code. Ein großer Wunsch von mir ist es, dass dieser in Zukunft automatisch generiert werden kann (entweder zur Lauf- oder Kompilierzeit).
Geplant ist auch noch die Unterstützung einer Konfiguration des verwendeten HTTP-Clients oder die Bereitstellung dessen durch die Anwendung. Für die Ausführung von Batches fehlt noch der Support für GET-Requests und die Auswertung der Batch-Response. Besondere Web API Anfragen wie Dis-Assoziationen für Lookup-Spalten könnten zusätzlich unterstützt werden.
Insgesamt befindet sich der Client noch in einer Alpha-Version.
An dieser Stelle möchte ich daher gerne nochmal darauf hinweisen, dass die gesamte Bibliothek als Open Source Projekt auf GitHub
veröffentlicht ist – Contributors welcome!