Udostępnij za pośrednictwem


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 klasach jest tworzony niejawnie przed procesem jednostki 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 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 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>();
}

Ostrzeżenie

Podczas pisania jednostek, które pochodzą z ITaskEntity lub TaskEntity<TState>, ważne jest, aby nie nazywać metody wyzwalania jednostki jako . Powoduje to błędy środowiska uruchomieniowego podczas wywoływania jednostki, ponieważ istnieje niejednoznaczne dopasowanie z nazwą metody "RunAsync", ponieważ ITaskEntity już definiuje metodę "RunAsync" na poziomie instancji.

Usuwanie jednostek w modelu izolowanym

Usunięcie jednostki w modelu izolowanym jest realizowane przez ustawienie stanu jednostki na null, a ten proces 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że nie ma możliwości ustawienia stanu na null w przypadku trasy POCO, dlatego niejawnie zdefiniowana operacja usunięcia jest jedynym prawdziwym sposobem usunięcia.

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 wywoływana jako operacja musi spełniać inne wymagania:

  • Operacja musi mieć co najwyżej jeden argument, ale nie ma ż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 sygnalizuje, a następnie wywołuje encję.

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 sygnalizuje, a następnie wywołuje encję.

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 zapewnia, że nazwa i typ argumentu operacji są zgodne z zaimplementowanym elementem. Zaleca się używanie interfejsów, gdy jest to możliwe do uzyskiwania dostępu do jednostek.

Na przykład możemy zmodyfikować przykład licznika:

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ż wiele jednostek może zaimplementować interfejs, 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 . Na 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 zwraca Task<T>wartość , wartość parametru T jest zawsze równa null lub default zamiast rzeczywistego wyniku. Na przykład nie ma sensu sygnalizować Get operacji, ponieważ nie zwraca wartości. Zamiast tego klienci mogą użyć ReadStateAsync do bezpośredniego dostępu do stanu licznika lub uruchomić funkcję orkiestratora, która wywołuje operację Get.

Przykład: Orkiestracja najpierw wysyła sygnały, a następnie wywołuje entytet 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ć przez zdefiniowanie identyfikatora jednostki lub tylko klucza jednostki, gdy istnieje tylko jedna klasa, która implementuje jednostkę:

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. Obowiązują również następujące reguły:

  • 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, które zwracają Task lub Task<T>, mogą być wywoływane lub sygnalizowane. W przypadku wywołania mogą zwracać wynik operacji lub z powrotem zgłaszać wyjątki zgłoszone przez operację. Jeśli zostaną zasygnalizowane, zwracają wartość domyślną, a nie rzeczywisty wynik lub wyjątek z operacji.

To nie jest obecnie obsługiwane w izolowanym środowisku 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 zamieszczamy kilka atrybutów, aby uwidocznić bazową serializację.

  • 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ą stanu utrwalonej jednostki i określimy 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ć atrybutów [DataContract] lub wcale ich nie używać.

[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. Często istnieje możliwość poprawnego radzenia sobie ze zmianą formatów danych, o ile rozumie się proces deserializacji używany przez JsonConvert.PopulateObjectprogram . Poniżej przedstawiono przykłady zmian i ich wpływu:

  • Jeśli nowa właściwość nie istnieje w przechowywanym formacie JSON, przyjmuje wartość domyślną.
  • Gdy właściwość obecna w przechowywanym formacie JSON zostanie usunięta, poprzednia zawartość zostanie utracona.
  • Po zmianie nazwy właściwości efekt polega na usunięciu starego i dodaniu nowego.
  • Gdy typ właściwości zostanie zmieniony, aby nie można było wykonać deserializacji z przechowywanego kodu JSON, zgłaszany jest wyjątek.
  • Gdy typ właściwości zostanie zmieniony tak, aby wciąż można było przeprowadzić deserializację z przechowywanego kodu JSON, zostaje to wykonane.

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. Aby uzyskać więcej informacji, zobacz Dostosowywanie serializacji i deserializacji.

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 DispatchAsync<T> obiekty 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 Pojęcia dotyczące wyzwalaczy i powiązań 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 jednostek:

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.

W poniższym przykładzie pokazano, jak skonfigurować HttpClient w pliku program.cs do późniejszego zaimportowania w klasie 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();
    }
}

W poniższym przykładzie pokazano, jak dołączyć wstrzykniętą usługę do klasy jednostki:

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, które przechowują wprowadzone 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 punktu wejścia funkcji, który nie jest statyczny, 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ący członkowie zawierają informacje o bieżącej operacji i pomagają 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 wywołała operację

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

  • HasState: jeśli jednostka istnieje; tj. ma jakiś stan
  • GetState<TState>(): pobiera bieżący stan jednostki i tworzy go, jeśli nie istnieje
  • SetState(arg): Tworzy lub aktualizuje stan jednostki
  • DeleteState(): usuwa stan jednostki, jeśli istnieje

Jeśli stan zwrócony przez GetState jest obiektem, kod aplikacji może go zmodyfikować. 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ący członkowie sygnalizują inne jednostki albo rozpoczynają nowe orkiestracje.

  • SignalEntity(EntityId, operation, input): wysyła jednokierunkowy komunikat do jednostki
  • CreateNewOrchestration(orchestratorFunctionName, input): Rozpoczyna 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