Przewodnik dewelopera dotyczący trwałych jednostek na platformie .NET

W tym artykule szczegółowo opisano dostępne interfejsy do tworzenia trwałych jednostek za pomocą platformy .NET, w tym przykłady i ogólne porady.

Funkcje jednostek zapewniają deweloperom aplikacji bezserwerowych wygodny sposób organizowania stanu aplikacji jako kolekcji precyzyjnych jednostek. Aby uzyskać więcej informacji na temat podstawowych pojęć, zobacz artykuł Durable Entities: Concepts (Trwałe jednostki: pojęcia).

Obecnie oferujemy dwa interfejsy API do definiowania jednostek:

  • Składnia oparta na klasach reprezentuje jednostki i operacje jako klasy i metody. Ta składnia umożliwia łatwe odczytywanie kodu i umożliwia wywoływanie operacji w sposób sprawdzony przez typy za pośrednictwem interfejsów.

  • Składnia oparta na funkcjach jest interfejsem niższego poziomu, który reprezentuje jednostki jako funkcje. Zapewnia precyzyjną kontrolę nad sposobem wysyłania operacji jednostki oraz sposobem zarządzania stanem jednostki.

Ten artykuł koncentruje się głównie na składni opartej na klasie, ponieważ oczekujemy, że będzie lepiej odpowiedni dla większości aplikacji. Jednak składnia oparta na funkcjach może być odpowiednia dla aplikacji, które chcą definiować własne abstrakcje dla stanu i operacji jednostki lub zarządzać nimi. Ponadto można go stosować do implementowania bibliotek, które wymagają ogólnej jakości, która nie jest obecnie obsługiwana przez składnię opartą na klasach.

Uwaga

Składnia oparta na klasach jest tylko warstwą na podstawie składni opartej na funkcji, więc oba warianty mogą być używane zamiennie w tej samej aplikacji.

Definiowanie klas jednostek

Poniższy przykład to implementacja Counter jednostki, która przechowuje pojedynczą wartość typu liczba całkowita i oferuje cztery operacje Add, , ResetGeti Delete.

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

Funkcja Run zawiera standardowy element wymagany do używania składni opartej na klasach. Musi to być statyczna funkcja platformy Azure. Jest wykonywany raz dla każdego komunikatu operacji, który jest przetwarzany przez jednostkę. Gdy DispatchAsync<T> jest wywoływana, a jednostka nie jest jeszcze w pamięci, tworzy obiekt typu T i wypełnia pola z ostatniego utrwalonego kodu JSON w magazynie (jeśli istnieje). Następnie wywołuje metodę o pasującej nazwie.

Funkcja EntityTrigger w Run tym przykładzie nie musi znajdować się w samej klasie Entity. Może ona znajdować się w dowolnej prawidłowej lokalizacji dla funkcji platformy Azure: wewnątrz przestrzeni nazw najwyższego poziomu lub wewnątrz klasy najwyższego poziomu. Jeśli jednak zagnieżdżona głębsza (np. funkcja jest zadeklarowana wewnątrz zagnieżdżonej klasy), ta funkcja nie zostanie rozpoznana przez najnowsze środowisko uruchomieniowe.

Uwaga

Stan jednostki opartej na klasie jest tworzony niejawnie przed procesem operacji przez jednostkę i może zostać jawnie usunięty w operacji przez wywołanie metody Entity.Current.DeleteState().

Uwaga

Aby uruchamiać jednostki w izolowanym modelu, potrzebna jest wersja 4.0.5455 narzędzi Azure Functions Core Tools lub nowsza.

Istnieją dwa sposoby definiowania jednostki jako klasy w modelu izolowanego procesu roboczego języka C#. Tworzą jednostki z różnymi strukturami serializacji stanu.

W przypadku następującego podejścia cały obiekt jest serializowany podczas definiowania jednostki.

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

Implementacja TaskEntity<TState>oparta na architekturze, która ułatwia korzystanie z wstrzykiwania zależności. W takim przypadku stan jest deserializowany do State właściwości, a żadna inna właściwość nie jest serializowana/deserializowana.

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

Ostrzeżenie

Podczas pisania jednostek, które pochodzą z ITaskEntity lub TaskEntity<TState>, ważne jest, aby nie nazywać metody RunAsyncwyzwalacza jednostki . Spowoduje to błędy środowiska uruchomieniowego podczas wywoływania jednostki, ponieważ istnieje niejednoznaczne dopasowanie z nazwą metody "RunAsync" z powodu ITaskEntity już zdefiniowania wystąpienia "RunAsync".

Usuwanie jednostek w modelu izolowanym

Usunięcie jednostki w modelu izolowanym jest realizowane przez ustawienie stanu jednostki na null. Sposób wykonania tej czynności zależy od używanej ścieżki implementacji jednostki.

  • W przypadku wyprowadzania z ITaskEntity składni opartej na funkcji lub używania metody usuń jest wykonywane przez wywołanie metody TaskEntityOperation.State.SetState(null).
  • Podczas wyprowadzania z TaskEntity<TState>metody usuń jest niejawnie definiowane. Można go jednak zastąpić, definiując metodę Delete w jednostce. Stan można również usunąć z dowolnej operacji za pomocą polecenia this.State = null.
    • Aby usunąć, ustawiając stan na null, musi TState mieć wartość null.
    • Niejawnie zdefiniowana operacja usuwania usuwa niepustą wartość TState.
  • W przypadku używania funkcji POCO jako stanu (nie pochodnego z TaskEntity<TState>), usuwanie jest definiowane niejawnie. Można zastąpić operację usuwania przez zdefiniowanie metody Delete w obiekcie POCO. Jednak nie ma możliwości ustawienia stanu na null w trasie POCO, więc niejawnie zdefiniowana operacja usuwania jest jedynym prawdziwym usunięciem.

Wymagania dotyczące klasy

Klasy jednostek to obiekty POC (zwykłe stare obiekty CLR), które nie wymagają specjalnych superklas, interfejsów ani atrybutów. Jednak:

  • Klasa musi być konstruowana (zobacz Konstrukcja jednostki).
  • Klasa musi być serializowalna w formacie JSON (zobacz Serializacja jednostek).

Ponadto każda metoda, która ma być wywoływana jako operacja, musi spełniać inne wymagania:

  • Operacja musi mieć co najwyżej jeden argument i nie może mieć żadnych przeciążeń ani argumentów typu ogólnego.
  • Operacja przeznaczona do wywołania z orkiestracji przy użyciu interfejsu musi zwrócić Task wartość lub Task<T>.
  • Argumenty i wartości zwracane muszą być wartościami z możliwością serializacji lub obiektami.

Co może zrobić operacje?

Wszystkie operacje jednostki mogą odczytywać i aktualizować stan jednostki, a zmiany stanu są automatycznie utrwalane w magazynie. Ponadto operacje mogą wykonywać zewnętrzne operacje we/wy lub inne obliczenia w ramach ogólnych limitów wspólnych dla wszystkich funkcji platformy Azure.

Operacje mają również dostęp do funkcji udostępnianych Entity.Current przez kontekst:

  • EntityName: nazwa aktualnie wykonywanej jednostki.
  • EntityKey: klucz aktualnie wykonywanej jednostki.
  • EntityId: identyfikator aktualnie wykonywanej jednostki (łącznie z nazwą i kluczem).
  • SignalEntity: wysyła jednokierunkowy komunikat do jednostki.
  • CreateNewOrchestration: uruchamia nową aranżację.
  • DeleteState: usuwa stan tej jednostki.

Na przykład możemy zmodyfikować jednostkę licznika, aby uruchamiała aranżację, gdy licznik osiągnie 100 i przekazuje identyfikator jednostki jako argument wejściowy:

public void Add(int amount) 
{
    if (this.Value < 100 && this.Value + amount >= 100)
    {
        Entity.Current.StartNewOrchestration("MilestoneReached", Entity.Current.EntityId);
    }
    this.Value += amount;      
}

Uzyskiwanie bezpośredniego dostępu do jednostek

Dostęp do jednostek opartych na klasach można uzyskać bezpośrednio przy użyciu jawnych nazw ciągów dla jednostki i jej operacji. Ta sekcja zawiera przykłady. Aby uzyskać dokładniejsze wyjaśnienie podstawowych pojęć (takich jak sygnały i wywołania), zobacz dyskusję w temacie Jednostki programu Access.

Uwaga

Jeśli to możliwe, należy uzyskać dostęp do jednostek za pośrednictwem interfejsów, ponieważ zapewnia on więcej kontroli typów.

Przykład: jednostka sygnałów klienta

Poniższa funkcja Http platformy Azure implementuje operację DELETE przy użyciu konwencji REST. Wysyła sygnał usuwania do jednostki licznika, której klucz jest przekazywany w ścieżce adresu URL.

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

Przykład: klient odczytuje stan jednostki

Poniższa funkcja Http platformy Azure implementuje operację GET przy użyciu konwencji REST. Odczytuje bieżący stan jednostki licznika, której klucz jest przekazywany w ścieżce adresu URL.

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

Uwaga

Obiekt zwracany przez ReadEntityStateAsync jest tylko kopią lokalną, czyli migawką stanu jednostki z jakiegoś wcześniejszego punktu w czasie. W szczególności może być nieaktualny i modyfikowanie tego obiektu nie ma wpływu na rzeczywistą jednostkę.

Przykład: orkiestracja najpierw sygnałów, a następnie wywołuje jednostkę

Poniższa aranżacja sygnalizuje jednostkę licznika, aby ją zwiększać, a następnie wywołuje tę samą jednostkę, aby odczytać jego najnowszą wartość.

[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;
}

Przykład: jednostka sygnałów klienta

Poniższa funkcja Http platformy Azure implementuje operację DELETE przy użyciu konwencji REST. Wysyła sygnał usuwania do jednostki licznika, której klucz jest przekazywany w ścieżce adresu URL.

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

Przykład: klient odczytuje stan jednostki

Poniższa funkcja Http platformy Azure implementuje operację GET przy użyciu konwencji REST. Odczytuje bieżący stan jednostki licznika, której klucz jest przekazywany w ścieżce adresu URL.

[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;
}

Przykład: orkiestracja najpierw sygnałów, a następnie wywołuje jednostkę

Poniższa aranżacja sygnalizuje jednostkę licznika, aby ją zwiększać, a następnie wywołuje tę samą jednostkę, aby odczytać jego najnowszą wartość.

[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; 
}

Uzyskiwanie dostępu do jednostek za pośrednictwem interfejsów

Interfejsy mogą służyć do uzyskiwania dostępu do jednostek za pośrednictwem wygenerowanych obiektów serwera proxy. Takie podejście gwarantuje, że nazwa i typ argumentu operacji są zgodne z zaimplementowaną operacją. Zalecamy używanie interfejsów do uzyskiwania dostępu do jednostek zawsze, gdy jest to możliwe.

Na przykład możemy zmodyfikować przykład licznika w następujący sposób:

public interface ICounter
{
    void Add(int amount);
    Task Reset();
    Task<int> Get();
    void Delete();
}

public class Counter : ICounter
{
    ...
}

Klasy jednostek i interfejsy jednostek są podobne do interfejsów ziarna i ziarna spopularyzowane przez Orlean. Aby uzyskać więcej informacji na temat podobieństw i różnic między jednostkami trwałymi a Orleanem, zobacz Porównanie z aktorami wirtualnymi.

Oprócz sprawdzania typów interfejsy są przydatne w celu lepszego rozdzielenia problemów w aplikacji. Na przykład ponieważ jednostka może implementować wiele interfejsów, jedna jednostka może obsługiwać wiele ról. Ponadto, ponieważ interfejs może być implementowany przez wiele jednostek, ogólne wzorce komunikacji można zaimplementować jako biblioteki wielokrotnego użytku.

Przykład: jednostka klienta sygnalizuje za pośrednictwem interfejsu

Kod klienta może służyć SignalEntityAsync<TEntityInterface> do wysyłania sygnałów do jednostek, które implementują TEntityInterfaceprogram . Przykład:

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

W tym przykładzie proxy parametr jest dynamicznie generowanym wystąpieniem ICounterklasy , które wewnętrznie tłumaczy wywołanie na Delete sygnał.

Uwaga

Interfejsy SignalEntityAsync API mogą być używane tylko w przypadku operacji jednokierunkowych. Nawet jeśli operacja zwróci Task<T>wartość , wartość parametru T będzie zawsze mieć wartość null lub default, a nie rzeczywisty wynik. Na przykład nie ma sensu sygnalizować Get operacji, ponieważ żadna wartość nie jest zwracana. Zamiast tego klienci mogą uzyskiwać ReadStateAsync bezpośredni dostęp do stanu licznika lub uruchamiać funkcję orkiestratora, która wywołuje operację Get .

Przykład: najpierw orkiestracja sygnałów wywołuje jednostkę za pośrednictwem serwera proxy

Aby wywołać lub zasygnalizować jednostkę z poziomu orkiestracji, CreateEntityProxy można użyć jej wraz z typem interfejsu w celu wygenerowania serwera proxy dla jednostki. Ten serwer proxy może następnie służyć do wywoływania operacji lub sygnału:

[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;
}

Niejawnie wszystkie zwrócone void operacje są sygnalizowane i wszystkie operacje, które zwracają Task lub Task<T> są wywoływane. Można zmienić to domyślne zachowanie i sygnalizować operacje, nawet jeśli zwracają zadanie, używając SignalEntity<IInterfaceType> jawnie metody .

Krótsza opcja określania obiektu docelowego

Podczas wywoływania lub sygnalizowania jednostki przy użyciu interfejsu pierwszy argument musi określać jednostkę docelową. Element docelowy można określić, określając identyfikator jednostki lub w przypadkach, gdy istnieje tylko jedna klasa, która implementuje jednostkę, tylko klucz jednostki:

context.SignalEntity<ICounter>(new EntityId(nameof(Counter), "myCounter"), ...);
context.SignalEntity<ICounter>("myCounter", ...);

Jeśli określono tylko klucz jednostki i nie można odnaleźć unikatowej implementacji w czasie wykonywania, InvalidOperationException jest zgłaszany.

Ograniczenia dotyczące interfejsów jednostek

Jak zwykle wszystkie typy parametrów i zwracanych muszą być serializowalne w formacie JSON. W przeciwnym razie wyjątki serializacji są zgłaszane w czasie wykonywania.

Wymusimy również kilka reguł:

  • Interfejsy jednostek muszą być zdefiniowane w tym samym zestawie co klasa jednostki.
  • Interfejsy jednostek muszą definiować tylko metody.
  • Interfejsy jednostek nie mogą zawierać parametrów ogólnych.
  • Metody interfejsu jednostki nie mogą mieć więcej niż jednego parametru.
  • Metody interfejsu jednostki muszą zwracać wartości void, Tasklub Task<T>.

Jeśli którekolwiek z tych reguł zostanie naruszone, obiekt jest zgłaszany w czasie wykonywania, InvalidOperationException gdy interfejs jest używany jako argument typu do SignalEntity, SignalEntityAsynclub CreateEntityProxy. Komunikat o wyjątku wyjaśnia, która reguła została przerwana.

Uwaga

Metody interfejsu zwracane void mogą być tylko zasygnalizowane (jednokierunkowe), a nie wywoływane (dwukierunkowe). Metody interfejsu zwracane Task lub Task<T> mogą być wywoływane lub sygnalizowane. W przypadku wywołania zwracają wynik operacji lub ponownie zgłaszają wyjątki zgłoszone przez operację. Jednak w przypadku zasygnaliwowania nie zwracają rzeczywistego wyniku lub wyjątku od operacji, ale tylko wartość domyślna.

Nie jest to obecnie obsługiwane w izolowanym procesie roboczym platformy .NET.

Serializacja jednostek

Ponieważ stan jednostki jest trwały, klasa jednostki musi być serializowana. W tym celu środowisko uruchomieniowe Durable Functions używa biblioteki Json.NET , która obsługuje zasady i atrybuty w celu kontrolowania procesu serializacji i deserializacji. Najczęściej używane typy danych języka C# (w tym tablice i typy kolekcji) są już serializowalne i mogą być łatwo używane do definiowania stanu jednostek trwałych.

Na przykład Json.NET można łatwo serializować i deserializować następującą klasę:

[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;
    }

    ...
}

Atrybuty serializacji

W powyższym przykładzie wybraliśmy uwzględnienie kilku atrybutów w celu zwiększenia widoczności bazowej serializacji:

  • Dodajemy adnotację do klasy, [JsonObject(MemberSerialization.OptIn)] aby przypomnieć nam, że klasa musi być serializowalna i utrwalać tylko elementy członkowskie, które są jawnie oznaczone jako właściwości JSON.
  • Dodajemy adnotacje do pól, które mają być utrwalane [JsonProperty("name")] , aby przypomnieć nam, że pole jest częścią utrwalonego stanu jednostki i określić nazwę właściwości, która ma być używana w reprezentacji JSON.

Jednak te atrybuty nie są wymagane; inne konwencje lub atrybuty są dozwolone tak długo, jak działają z Json.NET. Można na przykład użyć [DataContract] atrybutów lub żadnych atrybutów:

[DataContract]
public class Counter
{
    [DataMember]
    public int Value { get; set; }
    ...
}

public class Counter
{
    public int Value;
    ...
}

Domyślnie nazwa klasy nie jest* przechowywana jako część reprezentacji JSON: oznacza to, że używamy TypeNameHandling.None jako ustawienia domyślnego. To domyślne zachowanie można zastąpić przy użyciu atrybutów JsonObject lub .JsonProperty

Wprowadzanie zmian w definicjach klas

Podczas wprowadzania zmian w definicji klasy po uruchomieniu aplikacji wymagana jest pewna ostrożność, ponieważ przechowywany obiekt JSON nie może być już zgodny z nową definicją klasy. Mimo to często istnieje możliwość poprawnego radzenia sobie ze zmianą formatów danych, o ile rozumie się proces deserializacji używany przez JsonConvert.PopulateObjectprogram .

Oto na przykład kilka przykładów zmian i ich efektu:

  • Po dodaniu nowej właściwości, która nie jest obecna w przechowywanym formacie JSON, przyjmuje wartość domyślną.
  • Gdy właściwość zostanie usunięta, która jest obecna w przechowywanym formacie JSON, poprzednia zawartość zostanie utracona.
  • Po zmianie nazwy właściwości efekt jest taki, jakby usuwał stary i dodaje nowy.
  • Gdy typ właściwości zostanie zmieniony, aby nie można było już wykonać deserializacji z przechowywanego kodu JSON, zgłaszany jest wyjątek.
  • Gdy typ właściwości zostanie zmieniony, ale nadal może być deserializowany z przechowywanego kodu JSON, robi to.

Istnieje wiele opcji dostosowywania zachowania Json.NET. Aby na przykład wymusić wyjątek, jeśli przechowywany kod JSON zawiera pole, które nie znajduje się w klasie, określ atrybut JsonObject(MissingMemberHandling = MissingMemberHandling.Error). Istnieje również możliwość pisania kodu niestandardowego na potrzeby deserializacji, który może odczytywać dane JSON przechowywane w dowolnych formatach.

Domyślne zachowanie serializacji zostało zmienione z Newtonsoft.Json na System.Text.Json. Więcej informacji można znaleźć tutaj.

Budowa jednostek

Czasami chcemy mieć większą kontrolę nad sposobem konstruowania obiektów jednostek. Teraz opisano kilka opcji zmiany domyślnego zachowania podczas konstruowania obiektów jednostek.

Inicjowanie niestandardowe przy pierwszym dostępie

Czasami musimy wykonać specjalną inicjację przed wysłaniem operacji do jednostki, do którego nigdy nie uzyskiwano dostępu lub która została usunięta. Aby określić to zachowanie, można dodać warunkowy przed parametrem DispatchAsync:

[FunctionName(nameof(Counter))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
{
    if (!ctx.HasState)
    {
        ctx.SetState(...);
    }
    return ctx.DispatchAsync<Counter>();
}

Powiązania w klasach jednostek

W przeciwieństwie do zwykłych funkcji metody klasy jednostek nie mają bezpośredniego dostępu do powiązań wejściowych i wyjściowych. Zamiast tego dane powiązania muszą być przechwytywane w deklaracji funkcji punktu wejścia, a następnie przekazywane do DispatchAsync<T> metody. Wszystkie przekazane obiekty DispatchAsync<T> są przekazywane automatycznie do konstruktora klasy jednostki jako argument.

W poniższym przykładzie pokazano, jak CloudBlobContainer można udostępnić odwołanie z powiązania wejściowego obiektu blob do jednostki opartej na klasach.

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

Aby uzyskać więcej informacji na temat powiązań w usłudze Azure Functions, zobacz dokumentację Wyzwalacze i powiązania usługi Azure Functions.

Wstrzykiwanie zależności w klasach jednostek

Klasy jednostek obsługują wstrzykiwanie zależności usługi Azure Functions. W poniższym przykładzie pokazano, jak zarejestrować usługę IHttpClientFactory w jednostce opartej na klasach.

[assembly: FunctionsStartup(typeof(MyNamespace.Startup))]

namespace MyNamespace
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
        }
    }
}

Poniższy fragment kodu przedstawia sposób dołączania wprowadzonej usługi do klasy jednostki.

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

Inicjowanie niestandardowe przy pierwszym dostępie

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

Powiązania w klasach jednostek

W poniższym przykładzie pokazano, jak używać powiązania wejściowego obiektu blob w jednostce opartej na klasie.

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

Aby uzyskać więcej informacji na temat powiązań w usłudze Azure Functions, zobacz dokumentację Wyzwalacze i powiązania usługi Azure Functions.

Wstrzykiwanie zależności w klasach jednostek

Klasy jednostek obsługują wstrzykiwanie zależności usługi Azure Functions.

Poniżej pokazano, jak skonfigurować HttpClient element w program.cs pliku do zaimportowania w dalszej części klasy jednostki.

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

Poniżej przedstawiono sposób dołączania wprowadzonej usługi do klasy jednostek.

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

Uwaga

Aby uniknąć problemów z serializacji, pamiętaj, aby wykluczyć pola przeznaczone do przechowywania wstrzymywane wartości z serializacji.

Uwaga

W przeciwieństwie do używania iniekcji konstruktora w regularnych funkcjach usługi Azure Functions, metoda punktu wejścia funkcji dla jednostek opartych na klasach musi być zadeklarowana static. Deklarowanie niestatycznego punktu wejścia funkcji może powodować konflikty między normalnym inicjatorem obiektu usługi Azure Functions a inicjatorem obiektu Durable Entities.

Składnia oparta na funkcjach

Do tej pory skupiliśmy się na składni opartej na klasie, ponieważ oczekujemy, że będzie ona lepiej odpowiednia dla większości aplikacji. Jednak składnia oparta na funkcjach może być odpowiednia dla aplikacji, które chcą definiować własne abstrakcje dla stanu i operacji jednostki lub zarządzać nimi. Ponadto może to być odpowiednie podczas implementowania bibliotek, które wymagają ogólnejlności, która nie jest obecnie obsługiwana przez składnię opartą na klasach.

Dzięki składni opartej na funkcji funkcja jawnie obsługuje wysyłanie operacji i jawnie zarządza stanem jednostki. Na przykład poniższy kod przedstawia jednostkę Counter zaimplementowaną przy użyciu składni opartej na funkcji.

[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;
    }
}

Obiekt kontekstu jednostki

Dostęp do funkcji specyficznych dla jednostki można uzyskać za pośrednictwem obiektu kontekstu typu IDurableEntityContext. Ten obiekt kontekstu jest dostępny jako parametr funkcji jednostki i za pośrednictwem właściwości Entity.Currentasync-local .

Następujące elementy członkowskie zawierają informacje o bieżącej operacji i pozwalają nam określić wartość zwracaną.

  • EntityName: nazwa aktualnie wykonywanej jednostki.
  • EntityKey: klucz aktualnie wykonywanej jednostki.
  • EntityId: identyfikator aktualnie wykonywanej jednostki (łącznie z nazwą i kluczem).
  • OperationName: nazwa bieżącej operacji.
  • GetInput<TInput>(): pobiera dane wejściowe dla bieżącej operacji.
  • Return(arg): zwraca wartość do orkiestracji, która nazwała operację.

Następujący członkowie zarządzają stanem jednostki (tworzenie, odczytywanie, aktualizowanie, usuwanie).

  • HasState: czy jednostka istnieje, czyli ma jakiś stan.
  • GetState<TState>(): pobiera bieżący stan jednostki. Jeśli jeszcze nie istnieje, zostanie on utworzony.
  • SetState(arg): tworzy lub aktualizuje stan jednostki.
  • DeleteState(): usuwa stan jednostki, jeśli istnieje.

Jeśli stan zwracany przez GetState element jest obiektem, można go bezpośrednio zmodyfikować za pomocą kodu aplikacji. Nie ma potrzeby ponownego wywoływania SetState na końcu (ale także bez szkody). Jeśli GetState<TState> jest wywoływany wiele razy, należy użyć tego samego typu.

Na koniec następujące elementy członkowskie są używane do sygnalizowania innych jednostek lub uruchamiania nowych aranżacji:

  • SignalEntity(EntityId, operation, input): wysyła jednokierunkowy komunikat do jednostki.
  • CreateNewOrchestration(orchestratorFunctionName, input): uruchamia nową aranżację.
[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;
    });
}

Następne kroki