Teilen über


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 Status einer klassenbasierten Entität wird implizit erstellt, bevor die Entität einen Vorgang verarbeitet und durch Aufrufen explizit in einem Vorgang Entity.Current.DeleteState() werden kann.

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 void Add(int amount) 
    {
        this.State += amount;
    }

    public Task 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, Ihre Entitätstriggermethode nicht zu benennen. 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

Das Löschen einer Entität im isolierten Modell erfolgt durch Festlegen des Entitätsstatus auf null, und dieser Prozess hängt vom verwendeten Entitätsimplementierungspfad ab:

  • 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 null in der POCO-Route festzulegen, sodass der implizit definierte Löschvorgang der einzige wahre Löschvorgang ist.

Klassenanforderungen

Entitätsklassen sind POCOs (einfache alte CLR-Objekte), die keine speziellen Superklassen, Schnittstellen oder Attribute erfordern. Allerdings:

Außerdem muss jede methode, die als Vorgang aufgerufen wird, andere Anforderungen erfüllen:

  • Ein Vorgang muss höchstens ein Argument haben, aber keine Überladungen oder generische 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: Die Orchestrierung signalisiert zuerst, dann ruft sie die 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: Die Orchestrierung signalisiert zuerst, dann ruft sie die 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 dem übereinstimmen, was implementiert wird. Es wird empfohlen, Schnittstellen zu verwenden, wenn möglich, um auf Entitäten zuzugreifen.

Beispielsweise können wir das Zählerbeispiel ändern:

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 mehrere Entitäten eine Schnittstelle implementieren können, 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 ein Vorgang Task<T> zurückgibt, ist der Wert des T Parameters immer null oder default statt des tatsächlichen Ergebnisses. Beispielsweise ist es nicht sinnvoll, den Get Vorgang zu signalisieren, da er keinen Wert zurückgibt. Stattdessen können Clients entweder ReadStateAsync verwenden, um direkt auf den Zählerstatus zuzugreifen, oder eine Orchestratorfunktion starten, die die Get-Operation aufruft.

Beispiel: Die Orchestrierung signalisiert zuerst und ruft dann die Entität über den Proxy 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. Das Ziel kann durch Definieren der Entitäts-ID oder nur des Entitätsschlüssels angegeben werden, wenn nur eine Klasse vorhanden ist, die die Entität implementiert:

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. Die folgenden Regeln gelten auch:

  • 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 signalisiert werden. Bei einem Aufruf wird das Ergebnis des Vorgangs zurückgegeben, oder vom Vorgang ausgelöste Ausnahmen werden erneut ausgelöst. Wenn signalisiert, geben sie den Standardwert und nicht das tatsächliche Ergebnis oder die Ausnahme des Vorgangs zurück.

Dies wird derzeit im isolierten .NET-Worker 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 fügen wir mehrere Attribute ein, um die zugrunde liegende Serialisierung sichtbarer 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 Felder, die beibehalten [JsonProperty("name")] werden sollen, um daran zu erinnern, dass ein Feld Teil des beibehaltenen 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. Beispielsweise kann man [DataContract] Attribute oder gar keine Attribute 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

Einige Sorgfalt ist erforderlich, wenn Änderungen an einer Klassendefinition vorgenommen werden, nachdem eine Anwendung ausgeführt wird, da das gespeicherte JSON-Objekt nicht mehr mit der neuen Klassendefinition übereinstimmen kann. Es ist oft möglich, korrekt mit veränderlichen Datenformaten umzugehen, solange man den von JsonConvert.PopulateObject verwendeten Deserialisierungsprozess versteht. Im Folgenden sind Beispiele für Änderungen und deren Auswirkungen aufgeführt:

  • Wenn eine neue Eigenschaft, die nicht im gespeicherten JSON vorhanden ist, hinzugefügt wird, wird der Standardwert angenommen.
  • Wenn eine eigenschaft, die im gespeicherten JSON vorhanden ist, entfernt wird, 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 ein Eigenschaftstyp geändert wird, damit er nicht aus dem gespeicherten JSON deserialisiert werden kann, wird eine Ausnahme ausgelöst.
  • Wenn ein Eigenschaftstyp geändert wird, damit er weiterhin aus dem gespeicherten JSON deserialisiert werden kann, geschieht dies.

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 unter Anpassen der Serialisierung und Deserialisierung.

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 Objekte, die an DispatchAsync<T> übergeben werden, werden automatisch als Argument an den Konstruktor der Entitätsklasse ü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-Funktionen finden Sie unter 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 veranschaulicht, wie der eingefügte Dienst in Ihre Entitätsklasse integriert wird:

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.

Im folgenden Beispiel wird veranschaulicht, wie Sie eine HttpClient in der program.cs-Datei konfigurieren, die später in der Entitätsklasse importiert werden soll.

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();
    }
}

Im folgenden Beispiel wird veranschaulicht, wie der eingefügte Dienst in die Entitätsklasse integriert wird:

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, müssen Sie Felder ausschließen, die eingefügte Werte aus der Serialisierung speichern.

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. Das Deklarieren eines funktionseinstiegspunkts, der nicht statisch ist, kann Konflikte zwischen dem normalen Azure Functions-Objektinitialisierer und dem Initialisierer für dauerhafte Entitäten verursachen.

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 Mitglieder bieten Informationen über die gegenwärtige Operation und unterstützen die Bestimmung eines Rückgabewerts.

  • EntityName: Der Name der Entität, die derzeit ausgeführt wird
  • EntityKey: Der Schlüssel der aktuell ausgeführten Entität
  • EntityId: Die ID der derzeit ausgeführten Entität (einschließlich 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.

Die folgenden Member verwalten den Status der Entität (erstellen, lesen, aktualisieren, löschen):

  • HasState: Wenn die Entität vorhanden ist; d. h., es gibt einen Zustand
  • GetState<TState>(): Ruft den aktuellen Status der Entität ab und erstellt einen, wenn er nicht vorhanden ist.
  • SetState(arg): Erstellt oder aktualisiert den Status der Entität.
  • DeleteState(): Löscht den Status der Entität, falls vorhanden.

Wenn der von GetState einem Objekt zurückgegebene Zustand ein Objekt ist, kann der Anwendungscode ihn ändern. 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 senden die folgenden Member Signale an andere Entitäten oder starten neue Orchestrierungen:

  • 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