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 nichtRunAsync 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 von TaskEntityOperation.State.SetState(null) ausgeführt.
  • Beim Ableiten von TaskEntity<TState> ist der Löschvorgang implizit definiert. Dieser Vorgang kann jedoch durch Definieren einer Delete-Methode für die Entität überschrieben werden. Der Zustand kann auch über this.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.
  • 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 einer Delete-Methode im POCO zu überschreiben. Es gibt jedoch keine Möglichkeit, den Zustand in der POCO-Route auf null 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:

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 oder Task<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 oder Task<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;
    });
}

Nächste Schritte