Entwicklerhandbuch für dauerhafte Entitäten in .NET
In diesem Artikel werden ausführlich die verfügbaren Schnittstellen für die Entwicklung dauerhafter Entitäten mit .NET beschrieben, z. B. mit Beispielen und in Form von allgemeinen Ratschlägen.
Entitätsfunktionen sind für Entwickler von serverlosen Anwendungen eine gute Möglichkeit, um den Anwendungszustand als Sammlung mit differenzierten Entitäten zu organisieren. Ausführlichere Informationen zu den zugrunde liegenden Konzepten finden Sie im Artikel zum Thema Dauerhafte Entitäten: Konzepte.
Zum Definieren von Entitäten stehen derzeit zwei APIs zur Verfügung:
Bei der klassenbasierten Syntax werden Entitäten und Vorgänge als Klassen und Methoden dargestellt. Mit dieser Syntax wird leicht lesbarer Code erstellt, und Vorgänge können mit Typüberprüfung über Schnittstellen aufgerufen werden.
Die funktionsbasierte Syntax ist eine Schnittstelle auf niedrigerer Ebene, auf der Entitäten als Funktionen dargestellt werden. Sie ermöglicht eine genaue Steuerung, wie die Entitätsvorgänge übermittelt werden und der Entitätszustand verwaltet wird.
In diesem Artikel geht es vorrangig um die klassenbasierte Syntax, weil wir erwarten, dass sie für die meisten Anwendungen besser geeignet ist. Die funktionsbasierte Syntax kann aber für Anwendungen geeignet sein, die eigene Abstraktionen für den Entitätszustand und die Entitätsvorgänge definieren oder verwalten. Sie kann sich auch für die Implementierung von Bibliotheken eignen, die eine generische Funktionalität erfordern, die von der klassenbasierten Syntax derzeit nicht unterstützt wird.
Hinweis
Da es sich bei der klassenbasierten Syntax lediglich um eine Schicht oberhalb der funktionsbasierten Syntax handelt, können beide Varianten in derselben Anwendung verwendet werden.
Definieren von Entitätsklassen
Das folgende Beispiel ist eine Implementierung einer Counter
-Entität, mit der ein einzelner Wert vom Typ „integer“ gespeichert wird und die über die vier Vorgänge Add
, Reset
, Get
und Delete
verfügt.
[JsonObject(MemberSerialization.OptIn)]
public class Counter
{
[JsonProperty("value")]
public int Value { get; set; }
public void Add(int amount)
{
this.Value += amount;
}
public Task Reset()
{
this.Value = 0;
return Task.CompletedTask;
}
public Task<int> Get()
{
return Task.FromResult(this.Value);
}
public void Delete()
{
Entity.Current.DeleteState();
}
[FunctionName(nameof(Counter))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
=> ctx.DispatchAsync<Counter>();
}
Die Funktion Run
enthält den Baustein, der für die Nutzung der klassenbasierten Syntax erforderlich ist. Hierbei muss es sich um eine statische Azure-Funktion handeln. Sie wird einmal pro Vorgangsmeldung ausgeführt, die von der Entität verarbeitet wird. Wenn DispatchAsync<T>
aufgerufen wird und sich die Entität nicht bereits im Arbeitsspeicher befindet, wird ein Objekt vom Typ T
erstellt, und die Felder werden mit den Daten aus dem letzten gespeicherten JSON-Code im Speicher gefüllt (falls vorhanden). Anschließend wird die Methode mit dem übereinstimmenden Namen aufgerufen.
Die EntityTrigger
-Funktion (in diesem Beispiel Run
) muss sich nicht in der Entity-Klasse selbst befinden. Sie kann sich an einem beliebigen gültigen Speicherort für eine Azure-Funktion befinden: im Namespace der obersten Ebene oder in einer Klasse der obersten Ebene. Wenn sie jedoch tiefer geschachtelt ist (und die Funktion beispielweise innerhalb einer geschachtelten Klasse deklariert), wird diese Funktion von der neuesten Runtime nicht erkannt.
Hinweis
Der Zustand einer klassenbasierten Entität wird implizit erstellt, bevor die Entität einen Vorgang verarbeitet, und kann per Vorgang explizit gelöscht werden, indem Entity.Current.DeleteState()
aufgerufen wird.
Hinweis
Sie benötigen mindestens Azure Functions Core Tools Version 4.0.5455
, um Entitäten im isolierten Modell auszuführen.
Es gibt zwei Möglichkeiten, im isolierten C#-Arbeitsmodell eine Entität als Klasse zu definieren. Sie erzeugen Entitäten mit unterschiedlichen Strukturen für die Zustandsserialisierung.
Mit der folgenden Vorgehensweise wird das gesamte Objekt serialisiert, wenn eine Entität definiert wird.
public class Counter
{
public int Value { get; set; }
public void Add(int amount)
{
this.Value += amount;
}
public Task Reset()
{
this.Value = 0;
return Task.CompletedTask;
}
public Task<int> Get()
{
return Task.FromResult(this.Value);
}
// Delete is implicitly defined when defining an entity this way
[Function(nameof(Counter))]
public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
=> dispatcher.DispatchAsync<Counter>();
}
Eine TaskEntity<TState>
-basierte Implementierung, die die Verwendung der Abhängigkeitsinjektion vereinfacht. In diesem Fall wird der Zustand in die Eigenschaft State
deserialisiert, und es wird keine andere Eigenschaft serialisiert/deserialisiert.
public class Counter : TaskEntity<int>
{
readonly ILogger logger;
public Counter(ILogger<Counter> logger)
{
this.logger = logger;
}
public int Add(int amount)
{
this.State += amount;
}
public Reset()
{
this.State = 0;
return Task.CompletedTask;
}
public Task<int> Get()
{
return Task.FromResult(this.State);
}
// Delete is implicitly defined when defining an entity this way
[Function(nameof(Counter))]
public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
=> dispatcher.DispatchAsync<Counter>();
}
Warnung
Beim Schreiben von Entitäten, die von ITaskEntity
oder TaskEntity<TState>
abgeleitet werden, ist es wichtig, die Entitätstriggermethode nicht RunAsync
zu nennen. Dies führt zu Laufzeitfehlern beim Aufrufen der Entität: Es gibt eine mehrdeutige Übereinstimmung mit dem Methodennamen „RunAsync“, weil ITaskEntity
bereits eine RunAsync-Methode auf Instanzebene definiert.
Löschen von Entitäten im isolierten Modell
Der Löschvorgang für eine Entität im isolierten Modell wird durch Festlegen des Entitätsstatus auf null
ausgeführt. Wie dies erreicht wird, hängt davon ab, welcher Implementierungspfad für die Entität verwendet wird.
- Bei der Ableitung von
ITaskEntity
oder der Verwendung einer funktionsbasierten Syntax wird der Löschvorgang durch Aufrufen vonTaskEntityOperation.State.SetState(null)
ausgeführt. - Beim Ableiten von
TaskEntity<TState>
ist der Löschvorgang implizit definiert. Dieser Vorgang kann jedoch durch Definieren einerDelete
-Methode für die Entität überschrieben werden. Der Zustand kann auch überthis.State = null
aus jedem beliebigen Vorgang gelöscht werden.- Zum Löschen durch Festlegen des Zustands auf NULL muss
TState
Nullwerte zulassen. - Der implizit definierte Löschvorgang löscht
TState
-Werte, die keine Nullwerte zulassen.
- Zum Löschen durch Festlegen des Zustands auf NULL muss
- Wenn Sie ein POCO als Status verwenden (ohne Ableitung von
TaskEntity<TState>
), ist der Löschvorgang implizit definiert. Es ist möglich, den Löschvorgang durch Definieren einerDelete
-Methode im POCO zu überschreiben. Es gibt jedoch keine Möglichkeit, den Zustand in der POCO-Route aufnull
festzulegen, sodass der implizit definierte Löschvorgang der einzige wahre Löschvorgang ist.
Klassenanforderungen
Entitätsklassen sind POCOs (Plain Old CLR Objects), für die keine speziellen übergeordneten Klassen, Schnittstellen oder Attribute erforderlich sind. Allerdings:
- Die Klasse muss erstellbar sein (siehe Entitätserstellung).
- Die Klasse muss JSON-serialisierbar sein (siehe Entitätsserialisierung).
Außerdem muss jede Methode, die als Vorgang aufgerufen werden soll, andere Anforderungen erfüllen:
- Ein Vorgang darf maximal über ein Argument verfügen und darf keine Überladungen oder generischen Typargumente aufweisen.
- Ein Vorgang, der von einer Orchestrierung mit einer Schnittstelle aufgerufen werden soll, muss
Task
oderTask<T>
zurückgeben. - Argumente und Rückgabewerte müssen serialisierbare Werte oder Objekte sein.
Was gibt es mit Vorgängen für Möglichkeiten?
Alle Entitätsvorgänge können den Entitätszustand lesen und aktualisieren, und Änderungen des Zustands werden automatisch dauerhaft gespeichert. Darüber hinaus können mit Vorgängen externe E/A-Berechnungen oder andere Berechnungen durchgeführt werden, und zwar innerhalb der allgemeinen Grenzwerte, die für alle Azure-Funktionen gelten.
Für Vorgänge ist auch der Zugriff auf Funktionalität möglich, die über den Entity.Current
-Kontext bereitgestellt wird:
EntityName
: Der Name der Entität, die derzeit ausgeführt wird.EntityKey
: Der Schlüssel der Entität, die derzeit ausgeführt wird.EntityId
: Die ID der Entität, die derzeit ausgeführt wird (enthält Name und Schlüssel).SignalEntity
: Sendet eine unidirektionale Nachricht an eine Entität.CreateNewOrchestration
: Startet eine neue Orchestrierung.DeleteState
: Löscht den Zustand dieser Entität.
Wir können die Counter-Entität beispielsweise so modifizieren, dass eine Orchestrierung gestartet wird, wenn der Zähler den Wert 100 erreicht und die Entitäts-ID als Eingabeargument übergibt:
public void Add(int amount)
{
if (this.Value < 100 && this.Value + amount >= 100)
{
Entity.Current.StartNewOrchestration("MilestoneReached", Entity.Current.EntityId);
}
this.Value += amount;
}
Direktes Zugreifen auf Entitäten
Auf klassenbasierte Entitäten kann direkt zugegriffen werden, indem explizite Zeichenfolgennamen für die Entität und die zugehörigen Vorgänge verwendet werden. Dieser Abschnitt enthält Beispiele. Eine ausführlichere Beschreibung der zugrunde liegenden Konzepte (z. B. Signale und Aufrufe) finden Sie unter Zugreifen auf Entitäten.
Hinweis
Aufgrund der besseren Typüberprüfung sollten Sie nach Möglichkeit über Schnittstellen auf Entitäten zuzugreifen.
Beispiel: Client sendet Signal an Entität
Mit der folgenden Azure-HTTP-Funktion wird ein DELETE-Vorgang mit REST-Konventionen implementiert. Sie sendet ein Löschsignal an die Counter-Entität, deren Schlüssel im URL-Pfad übergeben wird.
[FunctionName("DeleteCounter")]
public static async Task<HttpResponseMessage> DeleteCounter(
[HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Counter/{entityKey}")] HttpRequestMessage req,
[DurableClient] IDurableEntityClient client,
string entityKey)
{
var entityId = new EntityId("Counter", entityKey);
await client.SignalEntityAsync(entityId, "Delete");
return req.CreateResponse(HttpStatusCode.Accepted);
}
Beispiel: Client liest Entitätszustand
Mit der folgenden Azure-HTTP-Funktion wird ein GET-Vorgang mit REST-Konventionen implementiert. Sie liest den aktuellen Zustand der Counter-Entität, deren Schlüssel im URL-Pfad übergeben wird.
[FunctionName("GetCounter")]
public static async Task<HttpResponseMessage> GetCounter(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "Counter/{entityKey}")] HttpRequestMessage req,
[DurableClient] IDurableEntityClient client,
string entityKey)
{
var entityId = new EntityId("Counter", entityKey);
var state = await client.ReadEntityStateAsync<Counter>(entityId);
return req.CreateResponse(state);
}
Hinweis
Das von ReadEntityStateAsync
zurückgegebene Objekt ist nur eine lokale Kopie, also eine Momentaufnahme des Entitätszustands zu einem früheren Zeitpunkt. Es kann auch veraltet sein, und eine Änderung dieses Objekts hat keinerlei Auswirkung auf die eigentliche Entität.
Beispiel: Orchestrierung sendet zunächst Signal und ruft anschließend Entität auf
Bei der folgenden Orchestrierung wird ein Signal an eine Counter-Entität gesendet, um diese zu inkrementieren, und anschließend wird dieselbe Entität aufgerufen, um den letzten Wert zu lesen.
[FunctionName("IncrementThenGet")]
public static async Task<int> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var entityId = new EntityId("Counter", "myCounter");
// One-way signal to the entity - does not await a response
context.SignalEntity(entityId, "Add", 1);
// Two-way call to the entity which returns a value - awaits the response
int currentValue = await context.CallEntityAsync<int>(entityId, "Get");
return currentValue;
}
Beispiel: Client sendet Signal an Entität
Mit der folgenden Azure-HTTP-Funktion wird ein DELETE-Vorgang mit REST-Konventionen implementiert. Sie sendet ein Löschsignal an die Counter-Entität, deren Schlüssel im URL-Pfad übergeben wird.
[Function("DeleteCounter")]
public static async Task<HttpResponseData> DeleteCounter(
[HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Counter/{entityKey}")] HttpRequestData req,
[DurableClient] DurableTaskClient client, string entityKey)
{
var entityId = new EntityInstanceId("Counter", entityKey);
await client.Entities.SignalEntityAsync(entityId, "Delete");
return req.CreateResponse(HttpStatusCode.Accepted);
}
Beispiel: Client liest Entitätszustand
Mit der folgenden Azure-HTTP-Funktion wird ein GET-Vorgang mit REST-Konventionen implementiert. Sie liest den aktuellen Zustand der Counter-Entität, deren Schlüssel im URL-Pfad übergeben wird.
[Function("GetCounter")]
public static async Task<HttpResponseData> GetCounter(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "Counter/{entityKey}")] HttpRequestData req,
[DurableClient] DurableTaskClient client, string entityKey)
{
var entityId = new EntityInstanceId("Counter", entityKey);
EntityMetadata<int>? entity = await client.Entities.GetEntityAsync<int>(entityId);
HttpResponseData response = request.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(entity.State);
return response;
}
Beispiel: Orchestrierung sendet zunächst Signal und ruft anschließend Entität auf
Bei der folgenden Orchestrierung wird ein Signal an eine Counter-Entität gesendet, um diese zu inkrementieren, und anschließend wird dieselbe Entität aufgerufen, um den letzten Wert zu lesen.
[Function("IncrementThenGet")]
public static async Task<int> Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
var entityId = new EntityInstanceId("Counter", "myCounter");
// One-way signal to the entity - does not await a response
await context.Entities.SignalEntityAsync(entityId, "Add", 1);
// Two-way call to the entity which returns a value - awaits the response
int currentValue = await context.Entities.CallEntityAsync<int>(entityId, "Get");
return currentValue;
}
Zugreifen auf Entitäten über Schnittstellen
Schnittstellen können zum Zugreifen auf Entitäten über generierte Proxyobjekte verwendet werden. Mit diesem Ansatz wird sichergestellt, dass der Name und der Argumenttyp eines Vorgangs mit den implementierten Komponenten übereinstimmen. Wir empfehlen Ihnen, nach Möglichkeit Schnittstellen für den Zugriff auf Entitäten zu verwenden.
Wir können das Counter-Beispiel beispielsweise wie folgt modifizieren:
public interface ICounter
{
void Add(int amount);
Task Reset();
Task<int> Get();
void Delete();
}
public class Counter : ICounter
{
...
}
Entitätsklassen und -schnittstellen ähneln den „Grains“ und Grainschnittstellen von Orleans. Weitere Informationen zu Ähnlichkeiten und Unterschieden zwischen dauerhaften Entitäten und Orleans finden Sie unter Vergleich mit virtuellen Akteuren.
Schnittstellen ermöglichen nicht nur die Typüberprüfung, sondern sind auch nützlich für eine bessere Trennung von Zuständigkeiten innerhalb der Anwendung. Beispiel: Da eine Entität mehrere Schnittstellen implementieren kann, kann eine einzelne Entität mehrere Rollen übernehmen. Da eine Schnittstelle zudem von mehreren Entitäten implementiert werden kann, können allgemeine Kommunikationsmuster als wiederverwendbare Bibliotheken implementiert werden.
Beispiel: Client sendet Signal per Schnittstelle an Entität
Für den Clientcode kann SignalEntityAsync<TEntityInterface>
verwendet werden, um Signale an Entitäten zu senden, die TEntityInterface
implementieren. Beispiel:
[FunctionName("DeleteCounter")]
public static async Task<HttpResponseMessage> DeleteCounter(
[HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Counter/{entityKey}")] HttpRequestMessage req,
[DurableClient] IDurableEntityClient client,
string entityKey)
{
var entityId = new EntityId("Counter", entityKey);
await client.SignalEntityAsync<ICounter>(entityId, proxy => proxy.Delete());
return req.CreateResponse(HttpStatusCode.Accepted);
}
In diesem Beispiel ist der Parameter proxy
eine dynamisch generierte Instanz von ICounter
, die den Aufruf von Delete
intern in ein Signal übersetzt.
Hinweis
Die SignalEntityAsync
-APIs können nur für unidirektionale Vorgänge verwendet werden. Auch wenn für einen Vorgang Task<T>
zurückgegeben wird, ist der Wert des Parameters T
immer null oder default
(nicht das tatsächliche Ergebnis).
Es ergibt beispielsweise keinen Sinn, ein Signal an den Get
-Vorgang zu senden, weil kein Wert zurückgegeben wird. Stattdessen können Clients entweder ReadStateAsync
verwenden, um direkt auf den Zählerzustand zuzugreifen, oder eine Orchestratorfunktion starten, mit der der Get
-Vorgang aufgerufen wird.
Beispiel: Orchestrierung sendet zunächst Signal und ruft anschließend per Proxy die Entität auf
Um eine Entität aus einer Orchestrierung aufzurufen oder ihr ein Signal zu senden, kann CreateEntityProxy
zusammen mit dem Schnittstellentyp verwendet werden, um einen Proxy für die Entität zu generieren. Dieser Proxy kann dann genutzt werden, um Vorgänge aufzurufen oder Signale dafür zu senden:
[FunctionName("IncrementThenGet")]
public static async Task<int> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var entityId = new EntityId("Counter", "myCounter");
var proxy = context.CreateEntityProxy<ICounter>(entityId);
// One-way signal to the entity - does not await a response
proxy.Add(1);
// Two-way call to the entity which returns a value - awaits the response
int currentValue = await proxy.Get();
return currentValue;
}
Implizit wird an alle Vorgänge, die void
zurückgeben, ein Signal gesendet, und alle Vorgänge, die Task
oder Task<T>
zurückgeben, werden aufgerufen. Sie können dieses Standardverhalten ändern und auch dann Signale an Vorgänge senden, wenn diese „Task“ zurückgeben, indem Sie die SignalEntity<IInterfaceType>
-Methode explizit verwenden.
Kürzere Option zum Angeben des Ziels
Wenn Sie eine Entität mit einer Schnittstelle aufrufen oder ihr darüber ein Signal senden, muss im ersten Argument die Zielentität angegeben werden. Sie können das Ziel angeben, indem Sie die Entitäts-ID bereitstellen. In Fällen, in denen die Entität nur mit einer Klasse implementiert wird, geben Sie nur den Entitätsschlüssel an:
context.SignalEntity<ICounter>(new EntityId(nameof(Counter), "myCounter"), ...);
context.SignalEntity<ICounter>("myCounter", ...);
Wenn nur der Entitätsschlüssel angegeben wird und zur Laufzeit keine eindeutige Implementierung gefunden werden kann, wird InvalidOperationException
ausgelöst.
Einschränkungen für Entitätsschnittstellen
Alle Parameter und Rückgabetypen müssen JSON-serialisierbar sein. Andernfalls werden zur Laufzeit Serialisierungsausnahmen ausgelöst.
Außerdem erzwingen wir einige weitere Regeln:
- Entitätsschnittstellen müssen in derselben Assembly wie die Entitätsklasse definiert werden.
- Entitätsschnittstellen dürfen nur Methoden definieren.
- Entitätsschnittstellen dürfen keine generischen Parameter enthalten.
- Entitätsschnittstellenmethoden dürfen nicht mehr als einen Parameter enthalten.
- Entitätsschnittstellenmethoden müssen
void
,Task
oderTask<T>
zurückgeben.
Falls eine dieser Regeln verletzt wird, wird zur Laufzeit eine InvalidOperationException
ausgelöst, wenn die Schnittstelle als Typargument für SignalEntity
, SignalEntityAsync
oder CreateEntityProxy
verwendet wird. In der Ausnahmemeldung ist beschrieben, welche Regel verletzt wurde.
Hinweis
Schnittstellenmethoden, die void
zurückgeben, können nur per Signal erreicht (unidirektional) und nicht aufgerufen (bidirektional) werden. Schnittstellenmethoden, die Task
oder Task<T>
zurückgeben, können entweder aufgerufen oder per Signal erreicht werden. Bei einem Aufruf wird das Ergebnis des Vorgangs zurückgegeben, oder vom Vorgang ausgelöste Ausnahmen werden erneut ausgelöst. Bei einer Signalisierung wird nicht das tatsächliche Ergebnis oder die Ausnahme des Vorgangs zurückgegeben, sondern nur der Standardwert.
Dies wird im isolierten .NET-Worker derzeit nicht unterstützt.
Entitätsserialisierung
Da der Zustand einer Entität dauerhaft gespeichert wird, muss die Entitätsklasse serialisierbar sein. Für die Durable Functions-Laufzeit wird zu diesem Zweck die JSON.NET-Bibliothek verwendet. Sie unterstützt Richtlinien und Attribute, um den Serialisierungs- und Deserialisierungsprozess zu steuern. Die am häufigsten verwendeten C#-Datentypen (z. B. Arrays und Sammlungstypen) sind bereits serialisierbar und können problemlos verwendet werden, um den Zustand von dauerhaften Entitäten zu definieren.
Mit JSON.NET kann die folgende Klasse leicht serialisiert und deserialisiert werden:
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class User
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("yearOfBirth")]
public int YearOfBirth { get; set; }
[JsonProperty("timestamp")]
public DateTime Timestamp { get; set; }
[JsonProperty("contacts")]
public Dictionary<Guid, Contact> Contacts { get; set; } = new Dictionary<Guid, Contact>();
[JsonObject(MemberSerialization = MemberSerialization.OptOut)]
public struct Contact
{
public string Name;
public string Number;
}
...
}
Serialisierungsattribute
Im obigen Beispiel haben wir mehrere Attribute eingefügt, um die zugrunde liegende Serialisierung besser sichtbar zu machen:
- Wir kommentieren die Klasse mit
[JsonObject(MemberSerialization.OptIn)]
, um daran zu erinnern, dass die Klasse serialisierbar sein muss und nur Member gespeichert werden sollen, die explizit als JSON-Eigenschaften gekennzeichnet sind. - Wir kommentieren die zu speichernden Felder mit
[JsonProperty("name")]
, um daran zu erinnern, dass ein Feld Teil des gespeicherten Entitätszustands ist, und um den Eigenschaftennamen anzugeben, der in der JSON-Darstellung verwendet werden soll.
Diese Attribute sind aber nicht obligatorisch. Andere Konventionen oder Attribute sind zulässig, solange sie mit JSON.NET funktionieren. Es besteht beispielsweise die Möglichkeit, [DataContract]
-Attribute oder gar keine Attribute zu verwenden:
[DataContract]
public class Counter
{
[DataMember]
public int Value { get; set; }
...
}
public class Counter
{
public int Value;
...
}
Standardmäßig wird der Name der Klasse nicht* als Teil der JSON-Darstellung gespeichert. Dies bedeutet, dass wir TypeNameHandling.None
als Standardeinstellung verwenden. Dieses Standardverhalten kann außer Kraft gesetzt werden, indem JsonObject
- oder JsonProperty
-Attribute verwendet werden.
Vornehmen von Änderungen an Klassendefinitionen
Gehen Sie mit Bedacht vor, wenn Sie Änderungen an einer Klassendefinition vornehmen, nachdem eine Anwendung ausgeführt wurde, da das gespeicherte JSON-Objekt möglicherweise nicht mehr mit der neuen Klassendefinition übereinstimmt. Trotzdem ist es häufig möglich, auf korrekte Weise mit sich ändernden Datenformaten zu arbeiten, solange Sie mit dem von JsonConvert.PopulateObject
verwendeten Deserialisierungsprozess vertraut sind.
Hier sind einige Beispiele für Änderungen und ihre Auswirkungen angegeben:
- Wenn eine neue Eigenschaft hinzugefügt wird, die im gespeicherten JSON-Code nicht enthalten ist, wird ihr Standardwert verwendet.
- Wenn eine Eigenschaft entfernt wird, die im gespeicherten JSON-Code enthalten ist, geht der vorherige Inhalt verloren.
- Wenn eine Eigenschaft umbenannt wird, wirkt sich dies so aus, als ob die alte Eigenschaft entfernt und eine neue hinzugefügt wird.
- Wenn der Typ einer Eigenschaft geändert wird, sodass sie nicht mehr aus dem gespeicherten JSON-Code deserialisiert werden kann, wird eine Ausnahme ausgelöst.
- Wenn der Typ einer Eigenschaft geändert wird, die Deserialisierung aus dem gespeicherten JSON-Code aber trotzdem noch möglich ist, wird dies durchgeführt.
Es sind viele Optionen verfügbar, mit denen Sie das Verhalten von JSON.NET anpassen können. Wenn Sie beispielsweise eine Ausnahme für den Fall erzwingen möchten, in dem der gespeicherte JSON-Code über ein Feld verfügt, das in der Klasse nicht enthalten ist, geben Sie das Attribut JsonObject(MissingMemberHandling = MissingMemberHandling.Error)
an. Sie können auch benutzerdefinierten Code für die Deserialisierung schreiben, mit dem JSON-Code, der in beliebigen Formaten gespeichert ist, gelesen werden kann.
Das Standardverhalten der Serialisierung hat sich von Newtonsoft.Json
in System.Text.Json
geändert. Weitere Informationen finden Sie hier.
Entitätserstellung
Es kann beispielsweise sein, dass besser gesteuert werden soll, wie Entitätsobjekte erstellt werden. Wir beschreiben nun verschiedene Optionen zum Ändern des Standardverhaltens beim Erstellen von Entitätsobjekten.
Benutzerdefinierte Initialisierung beim ersten Zugriff
Gelegentlich müssen wir eine spezielle Initialisierung durchführen, bevor ein Vorgang an eine Entität übermittelt wird, auf die nie zugegriffen oder die gelöscht wurde. Zum Angeben dieses Verhaltens kann vor DispatchAsync
eine Bedingung eingefügt werden:
[FunctionName(nameof(Counter))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
{
if (!ctx.HasState)
{
ctx.SetState(...);
}
return ctx.DispatchAsync<Counter>();
}
Bindungen in Entitätsklassen
Im Gegensatz zu regulären Funktionen haben Entitätsklassenmethoden keinen direkten Zugriff auf Eingabe- und Ausgabebindungen. Stattdessen müssen Bindungsdaten in der Einstiegspunkt-Funktionsdeklaration erfasst und anschließend an die Methode DispatchAsync<T>
übergeben werden. Alle an DispatchAsync<T>
übergebenen Objekte werden automatisch als Argument an den Entitätsklassenkonstruktor übergeben.
Im folgenden Beispiel wird gezeigt, wie ein Verweis vom Typ CloudBlobContainer
aus der Blobeingabebindung für eine klassenbasierte Entität verfügbar gemacht werden kann.
public class BlobBackedEntity
{
[JsonIgnore]
private readonly CloudBlobContainer container;
public BlobBackedEntity(CloudBlobContainer container)
{
this.container = container;
}
// ... entity methods can use this.container in their implementations ...
[FunctionName(nameof(BlobBackedEntity))]
public static Task Run(
[EntityTrigger] IDurableEntityContext context,
[Blob("my-container", FileAccess.Read)] CloudBlobContainer container)
{
// passing the binding object as a parameter makes it available to the
// entity class constructor
return context.DispatchAsync<BlobBackedEntity>(container);
}
}
Weitere Informationen zu Bindungen in Azure Functions finden Sie in der Dokumentation Konzepte für Azure Functions-Trigger und -Bindungen.
Abhängigkeitsinjektion in Entitätsklassen
Entitätsklassen unterstützen die Abhängigkeitsinjektion in Azure Functions. Das folgende Beispiel zeigt die Registrierung eines Diensts vom Typ IHttpClientFactory
bei einer klassenbasierten Entität:
[assembly: FunctionsStartup(typeof(MyNamespace.Startup))]
namespace MyNamespace
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddHttpClient();
}
}
}
Der folgende Codeausschnitt zeigt die Integration des eingefügten Diensts in Ihre Entitätsklasse:
public class HttpEntity
{
[JsonIgnore]
private readonly HttpClient client;
public HttpEntity(IHttpClientFactory factory)
{
this.client = factory.CreateClient();
}
public Task<int> GetAsync(string url)
{
using (var response = await this.client.GetAsync(url))
{
return (int)response.StatusCode;
}
}
[FunctionName(nameof(HttpEntity))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
=> ctx.DispatchAsync<HttpEntity>();
}
Benutzerdefinierte Initialisierung beim ersten Zugriff
public class Counter : TaskEntity<int>
{
protected override int InitializeState(TaskEntityOperation operation)
{
// This is called when state is null, giving a chance to customize first-access of entity.
return 10;
}
}
Bindungen in Entitätsklassen
Das folgende Beispiel zeigt, wie Sie eine Blobeingabebindung in einer klassenbasierten Entität verwenden.
public class BlobBackedEntity : TaskEntity<object?>
{
private BlobContainerClient Container { get; set; }
[Function(nameof(BlobBackedEntity))]
public Task DispatchAsync(
[EntityTrigger] TaskEntityDispatcher dispatcher,
[BlobInput("my-container")] BlobContainerClient container)
{
this.Container = container;
return dispatcher.DispatchAsync(this);
}
}
Weitere Informationen zu Bindungen in Azure Functions finden Sie in der Dokumentation Konzepte für Azure Functions-Trigger und -Bindungen.
Abhängigkeitsinjektion in Entitätsklassen
Entitätsklassen unterstützen die Abhängigkeitsinjektion in Azure Functions.
Das folgende Beispiel zeigt, wie Sie einen HttpClient
in der program.cs
-Datei konfigurieren, damit er später in der Entitätsklasse importiert wird.
public class Program
{
public static void Main()
{
IHost host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults((IFunctionsWorkerApplicationBuilder workerApplication) =>
{
workerApplication.Services.AddHttpClient<HttpEntity>()
.ConfigureHttpClient(client => {/* configure http client here */});
})
.Build();
host.Run();
}
}
Folgendermaßen wird der eingefügte Dienst in Ihre Entitätsklasse eingefügt.
public class HttpEntity : TaskEntity<object?>
{
private readonly HttpClient client;
public HttpEntity(HttpClient client)
{
this.client = client;
}
public async Task<int> GetAsync(string url)
{
using var response = await this.client.GetAsync(url);
return (int)response.StatusCode;
}
[Function(nameof(HttpEntity))]
public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
=> dispatcher.DispatchAsync<HttpEntity>();
}
Hinweis
Um Probleme mit der Serialisierung zu vermeiden, sollten Sie Felder ausschließen, die zum Speichern von injizierten Werten aus der Serialisierung dienen.
Hinweis
Anders als bei Verwendung der Konstruktorinjektion in regulären .NET-Azure-Funktionen muss die Funktionseinstiegspunkt-Methode für klassenbasierte Entitäten als static
deklariert werden. Wenn Sie einen nicht statischen Funktionseinstiegspunkt deklarieren, kann es zu Konflikten zwischen dem normalen Azure Functions-Objektinitialisierer und dem Durable Entities-Objektinitialisierer kommen.
Funktionsbasierte Syntax
Bisher ging es um die klassenbasierte Syntax, weil wir glauben, dass sie für die meisten Anwendungen besser geeignet ist. Die funktionsbasierte Syntax kann aber für Anwendungen geeignet sein, bei denen eigene Abstraktionen für den Entitätszustand und die Vorgänge definiert bzw. verwaltet werden sollen. Sie kann sich auch zur Implementierung von Bibliotheken eignen, die eine generische Funktionalität erfordern, die von der klassenbasierten Syntax derzeit nicht unterstützt wird.
Mit der funktionsbasierten Syntax wird die Vorgangsübermittlung explizit von der Entitätsfunktion verarbeitet und der Zustand der Entität explizit verwaltet. Im folgenden Code ist die Counter-Entität dargestellt, die mit der funktionsbasierten Syntax implementiert wird.
[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
switch (ctx.OperationName.ToLowerInvariant())
{
case "add":
ctx.SetState(ctx.GetState<int>() + ctx.GetInput<int>());
break;
case "reset":
ctx.SetState(0);
break;
case "get":
ctx.Return(ctx.GetState<int>());
break;
case "delete":
ctx.DeleteState();
break;
}
}
Entitätskontextobjekt
Auf die entitätsspezifische Funktionalität kann über ein Kontextobjekt vom Typ IDurableEntityContext
zugegriffen werden. Dieses Kontextobjekt ist auch als Parameter für die Entitätsfunktion und über die asynchrone lokale Entity.Current
-Eigenschaft verfügbar.
Die folgenden Member liefern Informationen zum aktuellen Vorgang und ermöglichen es uns, einen Rückgabewert anzugeben.
EntityName
: Der Name der Entität, die derzeit ausgeführt wird.EntityKey
: Der Schlüssel der Entität, die derzeit ausgeführt wird.EntityId
: Die ID der Entität, die derzeit ausgeführt wird (enthält Name und Schlüssel).OperationName
: Der Name des aktuellen Vorgangs.GetInput<TInput>()
: Ruft die Eingabe für den aktuellen Vorgang ab.Return(arg)
: Gibt einen Wert an die Orchestrierung zurück, die den Vorgang aufgerufen hat.
Mit den folgenden Membern wird der Zustand der Entität (Erstellen, Lesen, Aktualisieren, Löschen) verwaltet.
HasState
: Gibt an, ob die Entität vorhanden ist – also über einen Zustand verfügt.GetState<TState>()
: Ruft den aktuellen Zustand der Entität ab. Wenn sie noch nicht vorhanden ist, wird sie erstellt.SetState(arg)
: Erstellt oder aktualisiert den Zustand der Entität.DeleteState()
: Löscht den Zustand der Entität, falls er vorhanden ist.
Wenn der von GetState
zurückgegebene Zustand ein Objekt ist, ist eine direkte Änderung per Anwendungscode möglich. Es ist nicht erforderlich, am Ende SetState
aufzurufen (aber es schadet auch nicht). Wenn GetState<TState>
mehrere Male aufgerufen wird, muss der gleiche Typ verwendet werden.
Abschließend werden die folgenden Member verwendet, um Signale an andere Entitäten zu senden oder neue Orchestrierungen zu starten:
SignalEntity(EntityId, operation, input)
: Sendet eine unidirektionale Nachricht an eine Entität.CreateNewOrchestration(orchestratorFunctionName, input)
: Startet eine neue Orchestrierung.
[Function(nameof(Counter))]
public static Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
{
return dispatcher.DispatchAsync(operation =>
{
if (operation.State.GetState(typeof(int)) is null)
{
operation.State.SetState(0);
}
switch (operation.Name.ToLowerInvariant())
{
case "add":
int state = operation.State.GetState<int>();
state += operation.GetInput<int>();
operation.State.SetState(state);
return new(state);
case "reset":
operation.State.SetState(0);
break;
case "get":
return new(operation.State.GetState<int>());
case "delete":
operation.State.SetState(null);
break;
}
return default;
});
}