Guia do Desenvolvedor para entidades duráveis no .NET

Neste artigo, descrevemos em detalhes as interfaces disponíveis para desenvolver entidades duráveis com o .NET, incluindo exemplos e conselhos gerais.

As funções de entidade dão aos desenvolvedores de aplicativos sem servidor uma maneira conveniente de organizar o estado do aplicativo como uma coleção de entidades refinadas. Confira mais detalhes sobre os conceitos subjacentes no artigo Entidades Duráveis: Conceitos.

Atualmente, oferecemos duas APIs para definir entidades:

  • A sintaxe baseada em classe representa entidades e operações como classes e métodos. Essa sintaxe produz um código facilmente legível e permite que as operações sejam invocadas com uma verificação de tipo por meio de interfaces.

  • A sintaxe baseada em função é uma interface de nível inferior que representa as entidades como funções. Ela fornece controle preciso sobre como as operações de entidade são expedidas e como o estado da entidade é gerenciado.

Este artigo se concentra principalmente na sintaxe baseada em classe, pois antecipamos que seja a mais adequada para a maioria dos aplicativos. Porém, a sintaxe baseada em função pode ser apropriada para aplicativos que desejam definir ou gerenciar as próprias abstrações para estado e operações de entidades. Além disso, pode ser apropriada para implementar bibliotecas que exigem algo mais genérico, atualmente sem suporte da sintaxe baseada em classe.

Observação

A sintaxe baseada em classe é apenas uma camada sobre a sintaxe baseada em função, então essas duas variantes podem ser usadas de modo intercambiável no mesmo aplicativo.

Definir classes de entidade

O exemplo a seguir é uma implementação de uma entidade Counter que armazena um único valor do tipo inteiro e oferece quatro operações Add, Reset, Get e 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>();
}

A função Run contém o texto clichê necessário para usar a sintaxe baseada em classe. Ele deve ser uma Função do Azure estática. É executado uma vez para cada mensagem de operação processada pela entidade. Quando DispatchAsync<T> é chamado e a entidade ainda não está na memória, ela constrói um objeto do tipo T e preenche os campos com o último JSON persistido encontrado no armazenamento (se houver algum). Em seguida, invoca o método com o nome correspondente.

A função EntityTrigger, Run neste exemplo, não precisa residir dentro da própria classe Entity. Ele pode estar em qualquer local válido para uma função do Azure: dentro do namespace de nível superior ou dentro de uma classe de nível superior. No entanto, se aninhada mais profundamente (por exemplo, a Função é declarada dentro de uma classe aninhada ), então essa Função não será reconhecida pelo runtime mais recente.

Observação

O estado de uma entidade baseada em classe é criado implicitamente antes que a entidade processe uma operação e pode ser excluído de maneira explícita em uma operação ao chamar Entity.Current.DeleteState().

Observação

Você precisa do Azure Functions Core Tools versão 4.0.5455 ou superior para executar entidades no modelo isolado.

Há duas maneiras de definir uma entidade como uma classe no modelo de trabalho isolado em C#. Elas produzem entidades com estruturas de serialização de estado diferentes.

Com a abordagem a seguir, todo o objeto é serializado ao definir uma entidade.

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

Uma implementação baseada em TaskEntity<TState>, o que facilita o uso da injeção de dependência. Nesse caso, o estado é desserializado para a propriedade State e nenhuma outra propriedade é serializada ou desserializada.

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

Aviso

Ao escrever entidades que derivam de ITaskEntity ou de TaskEntity<TState>, é importante não nomear o método de gatilho de entidade RunAsync. Isso causará erros de runtime ao invocar a entidade, pois há uma correspondência ambígua com o nome do método "RunAsync" devido àoa fato de ITaskEntity já ter definido um "RunAsync" no nível da instância.

Como excluir entidades no modelo isolado

A exclusão de uma entidade no modelo isolado é realizada definindo o estado da entidade como null. Como isso é feito depende de qual caminho de implementação de entidade está sendo usado.

  • Ao derivar de ITaskEntity ou usar a sintaxe baseada em função, a exclusão é realizada chamando TaskEntityOperation.State.SetState(null).
  • Ao derivar de TaskEntity<TState>, a exclusão é definida implicitamente. No entanto, isso pode ser substituído definindo um método Delete na entidade. O estado também pode ser excluído de qualquer operação por meio de this.State = null.
    • Para excluir por meio do estado de configuração como nulo, será necessário que TState seja anulável.
    • A operação de exclusão definida implicitamente excluirá o TState não anulável.
  • Ao usar um POCO como seu estado (não derivando de TaskEntity<TState>), a exclusão é definida implicitamente. É possível substituir a operação de exclusão definindo um método Delete no POCO. No entanto, não há como definir o estado como null na rota POCO, portanto a operação de exclusão definida implicitamente é a única exclusão verdadeira.

Requisitos do Classe

As classes de entidade são POCOs (objetos CLR básicos) que não exigem superclasses, interfaces nem atributos especiais. No entanto:

Além disso, qualquer método destinado a ser invocado como uma operação deve atender a requisitos adicionais:

  • Uma operação deve ter no máximo um argumento e não deve ter sobrecargas ou argumentos de tipo genérico.
  • Uma operação destinada a ser chamada de uma orquestração que usa uma interface deve retornar Task ou Task<T>.
  • Os argumentos e os valores retornados devem ser valores ou objetos serializáveis.

O que as operações podem fazer?

Todas as operações de entidade podem ler e atualizar o estado da entidade, e as alterações no estado são persistidas automaticamente no armazenamento. Além disso, as operações podem executar E/S externas ou outros cálculos, dentro dos limites gerais comuns a todas as Azure Functions.

As operações também têm acesso à funcionalidade fornecida pelo contexto Entity.Current:

  • EntityName: o nome da entidade em execução no momento.
  • EntityKey: a chave da entidade em execução no momento.
  • EntityId: a ID da entidade atualmente em execução (inclui o nome e a chave).
  • SignalEntity: envia uma mensagem unidirecional para uma entidade.
  • CreateNewOrchestration: inicia uma nova orquestração.
  • DeleteState: exclui o estado desta entidade.

Por exemplo, podemos modificar a entidade do contador para que ela inicie uma orquestração quando o contador atingir 100 e passe a ID da entidade como um argumento de entrada:

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

Acessar entidades diretamente

As entidades baseadas em classe podem ser acessadas diretamente, ao usar nomes de cadeia de caracteres explícitos para a entidade e suas operações. Esta seção fornece modelos. Fornecemos alguns exemplos abaixo; para uma explicação mais detalhada dos conceitos subjacentes (como sinais versus chamadas), confira a discussão em Entidades de acesso.

Observação

Onde possível, é recomendável acessar entidades por meio de interfaces porque isso fornece mais verificação de tipo.

Exemplo: o cliente sinaliza uma entidade

A função HTTP do Azure a seguir implementa uma operação DELETE usando convenções REST. Ela envia um sinal de exclusão para a entidade do contador cuja chave é passada no caminho da 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);
}

Exemplo: o cliente lê um estado de entidade

A função HTTP do Azure a seguir implementa uma operação GET usando convenções REST. Ela lê o estado atual da entidade do contador cuja chave é passada no caminho da 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);
}

Observação

O objeto retornado por ReadEntityStateAsync é apenas uma cópia local, ou seja, um instantâneo do estado da entidade de algum momento anterior. Em particular, ele pode estar obsoleto, e a modificação desse objeto não tem efeito sobre a entidade real.

Exemplo: primeiro a orquestração sinaliza e depois chama a entidade

A orquestração a seguir sinaliza a uma entidade de contador para incrementá-la e depois chama a mesma entidade para ler seu valor mais recente.

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

Exemplo: o cliente sinaliza uma entidade

A função HTTP do Azure a seguir implementa uma operação DELETE usando convenções REST. Ela envia um sinal de exclusão para a entidade do contador cuja chave é passada no caminho da 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);
}

Exemplo: o cliente lê um estado de entidade

A função HTTP do Azure a seguir implementa uma operação GET usando convenções REST. Ela lê o estado atual da entidade do contador cuja chave é passada no caminho da 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;
}

Exemplo: primeiro a orquestração sinaliza e depois chama a entidade

A orquestração a seguir sinaliza a uma entidade de contador para incrementá-la e depois chama a mesma entidade para ler seu valor mais recente.

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

Acessar entidades por meio de interfaces

As interfaces podem ser usadas para acessar entidades por meio de objetos de proxy gerados. Essa abordagem garante que o nome e o tipo de argumento de uma operação correspondam ao que é implementado. É recomendável usar interfaces para acessar entidades sempre que possível.

Por exemplo, podemos modificar o exemplo de contador da seguinte maneira:

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

public class Counter : ICounter
{
    ...
}

Classes e interfaces de entidade são semelhantes às interfaces granulares e às granulares popularizadas por Orleans. Confira mais informações sobre semelhanças e diferenças entre Entidades Duráveis e Orleans em Comparação com atores virtuais.

Além de fornecer verificação de tipo, as interfaces são úteis para uma separação melhor das preocupações dentro do aplicativo. Por exemplo, como uma entidade pode implementar várias interfaces, uma única entidade pode atender a várias funções. Além disso, como uma interface pode ser implementada por várias entidades, os padrões de comunicação geral podem ser implementados como bibliotecas reutilizáveis.

Exemplo: o cliente sinaliza a entidade por meio da interface

O código do cliente pode usar SignalEntityAsync<TEntityInterface> para enviar sinais para as entidades que implementam TEntityInterface. Por exemplo:

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

Neste exemplo, o parâmetro proxy é uma instância gerada dinamicamente de ICounter, que converte internamente a chamada para Delete em um sinal.

Observação

As APIs SignalEntityAsync podem ser usadas somente para operações unidirecionais. Mesmo que uma operação retorne Task<T>, o valor do parâmetro T sempre será nulo ou default, não o resultado real. Por exemplo, não faz sentido sinalizar a operação Get, já que nenhum valor é retornado. Em vez disso, os clientes podem usar ReadStateAsync para acessar o estado do contador diretamente ou podem iniciar uma função de orquestrador que chama a operação Get.

Exemplo: primeiro a orquestração sinaliza e depois chama a entidade por meio do proxy

Para chamar ou sinalizar uma entidade de dentro de uma orquestração, CreateEntityProxy pode ser usado, junto com o tipo de interface, para gerar um proxy para a entidade. Esse proxy pode então ser usado para chamar ou sinalizar operações:

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

Implicitamente, todas as operações que retornam void são sinalizadas e todas as operações que retornam Task ou Task<T> são chamadas. É possível alterar esse comportamento padrão, e sinalizar as operações, mesmo que retornem Tarefa, usando o método SignalEntity<IInterfaceType> explicitamente.

Opção mais curta para especificar o destino

Ao chamar ou sinalizar uma entidade com o uso de uma interface, o primeiro argumento deve especificar a entidade de destino. O destino pode ser especificado ao indicar a ID da entidade ou, nos casos em que há apenas uma classe que implementa a entidade, apenas a chave de entidade:

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

Se apenas a chave de entidade for especificada e uma implementação exclusiva não puder ser encontrada em tempo de execução, InvalidOperationException será lançada.

Restrições em interfaces de entidade

Como de costume, todos os tipos de parâmetro e de retorno devem ser serializáveis em JSON. Caso contrário, as exceções de serialização são lançadas em tempo de execução.

Também aplicamos mais algumas regras:

  • As interfaces de entidade precisam ser definidas no mesmo assembly da classe de entidade.
  • As interfaces de entidade devem definir apenas métodos.
  • As interfaces de entidade não devem conter parâmetros genéricos.
  • Os métodos de interface de entidade não devem ter mais de um parâmetro.
  • Os métodos de interface de entidade precisam retornar void, Task ou Task<T>.

Se qualquer uma dessas regras for violada, uma InvalidOperationException será lançada em tempo de execução quando a interface for usada como um argumento de tipo para SignalEntity, SignalEntityAsync ou CreateEntityProxy. A mensagem de exceção explica qual regra foi quebrada.

Observação

Métodos de interface que retornam void só podem ser sinalizados (unidirecionais), não chamados (bidirecionais). Métodos de interface que retornam Task ou Task<T> podem ser chamados ou sinalizados. Se chamados, eles retornam o resultado da operação ou geram novamente exceções lançadas pela operação. No entanto, quando sinalizados, eles não retornam o resultado real ou a exceção da operação, mas apenas o valor padrão.

Atualmente, não há suporte para isso no trabalho isolado do .NET.

Serialização de entidade

Como o estado de uma entidade é permanentemente persistido, a classe de entidade deve ser serializável. O tempo de execução das Durable Functions usa a biblioteca Json.NET para essa finalidade, que dá suporte a várias políticas e atributos para controlar o processo de serialização e desserialização. Os tipos de dados C# mais usados (incluindo matrizes e tipos de coleção) já são serializáveis e podem ser usados facilmente para definir o estado de entidades duráveis.

Por exemplo, Json.NET pode serializar e desserializar facilmente a seguinte classe:

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

    ...
}

Atributos de serialização

No exemplo acima, optamos por incluir vários atributos para tornar a serialização subjacente mais visível:

  • Anotamos a classe com [JsonObject(MemberSerialization.OptIn)] para nos lembrar de que ela deve ser serializável e para persistir apenas os membros explicitamente marcados como propriedades JSON.
  • Anotamos os campos a serem persistidos com [JsonProperty("name")] para nos lembrar de que um campo faz parte do estado da entidade persistida e especificar o nome da propriedade a ser usada na representação JSON.

No entanto, esses atributos não são obrigatórios; outras convenções ou atributos são permitidos contanto que funcionem com Json.NET. Por exemplo, podem-se usar atributos [DataContract] ou nenhum atributo:

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

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

Por padrão, o nome da classe não é* armazenado como parte da representação JSON: ou seja, usamos TypeNameHandling.None como a configuração padrão. Esse comportamento padrão pode ser substituído usando atributos JsonObject ou JsonProperty.

Fazer alterações nas definições de classe

É necessário ter cuidado ao fazer alterações em uma definição de classe após a execução de um aplicativo, pois o objeto JSON armazenado pode não corresponder mais à nova definição de classe. Ainda assim, geralmente é possível lidar corretamente com a alteração dos formatos de dados, desde que se entenda o processo de desserialização usado por JsonConvert.PopulateObject.

Por exemplo, aqui estão alguns exemplos de alterações e seus efeitos:

  • Se uma nova propriedade for adicionada, que não esteja presente no JSON armazenado, ela assumirá seu valor padrão.
  • Quando uma propriedade é removida, que está presente no JSON armazenado, o conteúdo anterior é perdido.
  • Se uma propriedade for renomeada, o efeito seria parecido ao de remover a antiga e adicionar uma nova.
  • Se o tipo de uma propriedade for alterado para que ela não possa mais ser desserializada do JSON armazenado, uma exceção será lançada.
  • Quando o tipo de uma propriedade é alterado, mas ainda pode ser desserializado do JSON armazenado, ele faz isso.

Há muitas opções disponíveis para personalizar o comportamento de Json.NET. Por exemplo, para forçar uma exceção se o JSON armazenado contiver um campo que não está presente na classe, especifique o atributo JsonObject(MissingMemberHandling = MissingMemberHandling.Error). Também é possível escrever código personalizado para desserialização que pode ler JSON armazenado em formatos arbitrários.

O comportamento padrão de serialização foi alterado de Newtonsoft.Json para System.Text.Json. Para mais informações, consulte aqui.

Construção de entidade

Às vezes, queremos exercer mais controle sobre como os objetos de entidade são construídos. Descreveremos agora várias opções para alterar o comportamento padrão ao construir objetos de entidade.

Inicialização personalizada no primeiro acesso

Ocasionalmente, precisamos executar inicializações especiais antes de expedir uma operação para uma entidade que nunca foi acessada ou que foi excluída. Para especificar esse comportamento, é possível adicionar uma condicional antes de DispatchAsync:

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

Associações em classes de entidade

Ao contrário das funções regulares, os métodos de classe de entidade não têm acesso direto a associações de entrada e saída. Em vez disso, os dados de associação devem ser capturados na declaração da função de ponto de entrada e passados para o método DispatchAsync<T>. Todos os objetos passados para DispatchAsync<T> são passados automaticamente para o construtor de classe de entidade como um argumento.

O exemplo a seguir mostra como uma referência de CloudBlobContainer da associação de entrada de blobs pode ser disponibilizada em uma entidade baseada em classe.

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

Para obter mais informações sobre associações no Azure Functions, confira a documentação Gatilhos e Associações do Azure Functions.

Injeção de dependência em classes de entidade

As classes de entidade dão suporte à Injeção de Dependência do Azure Functions. O exemplo a seguir demonstra como registrar um serviço IHttpClientFactory em uma entidade baseada em classe.

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

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

O snippet a seguir demonstra como incorporar o serviço injetado à sua classe de entidade.

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

Inicialização personalizada no primeiro acesso

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

Associações em classes de entidade

O exemplo a seguir mostra como usar uma associação de entrada de blob em uma entidade baseada em classe.

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

Para obter mais informações sobre associações no Azure Functions, confira a documentação Gatilhos e Associações do Azure Functions.

Injeção de dependência em classes de entidade

As classes de entidade dão suporte à Injeção de Dependência do Azure Functions.

O exemplo a seguir demonstra como configurar um HttpClient no arquivo program.cs a ser importado posteriormente na classe de entidade.

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

Veja como incorporar o serviço injetado em sua classe de entidade.

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

Observação

Para evitar problemas com a serialização, exclua os campos destinados a armazenar valores injetados da serialização.

Observação

Diferentemente do que ocorre ao usar uma injeção de construtor no Azure Functions do .NET regular, o método de ponto de entrada de funções para entidades baseadas em classe deve ser declarado static. Declarar um ponto de entrada de função não estática pode causar conflitos entre o inicializador de objeto normal do Azure Functions e o inicializador de objeto de Entidades Duráveis.

Sintaxe baseada em função

Até o momento, nos concentramos na sintaxe baseada em classe, pois antecipamos que ela seja mais adequada para a maioria dos aplicativos. Porém, a sintaxe baseada em função pode ser apropriada para aplicativos que desejam definir ou gerenciar as próprias abstrações para estado e operações de entidades. Além disso, ela pode ser apropriada para implementar bibliotecas que exigem um nível genérico atualmente sem suporte da sintaxe baseada em classe.

Com a sintaxe baseada em função, a Função de Entidade manipula explicitamente a expedição da operação e gerencia de modo explícito o estado da entidade. Por exemplo, o código a seguir mostra a entidade Contador implementada com o uso da sintaxe baseada em função.

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

O objeto de contexto de entidade

A funcionalidade específica da entidade pode ser acessada por meio de um objeto de contexto do tipo IDurableEntityContext. Esse objeto de contexto está disponível como parâmetro para a função de entidade e por meio da propriedade local assíncrona Entity.Current.

Os membros a seguir fornecem informações sobre a operação atual e nos permitem especificar um valor de retorno.

  • EntityName: o nome da entidade em execução no momento.
  • EntityKey: a chave da entidade em execução no momento.
  • EntityId: a ID da entidade atualmente em execução (inclui o nome e a chave).
  • OperationName: o nome da operação atual.
  • GetInput<TInput>(): obtém a entrada da operação atual.
  • Return(arg): retorna um valor para a orquestração que chamou a operação.

Os membros a seguir gerenciam o estado da entidade (criar, ler, atualizar, excluir).

  • HasState: se a entidade existe, ou seja, se tem algum estado.
  • GetState<TState>(): obtém o estado atual da entidade. Se a tabela ainda não existir, ela será criada.
  • SetState(arg): cria ou atualiza o estado da entidade.
  • DeleteState(): exclui o estado da entidade, se ela existir.

Se o estado retornado por GetState for um objeto, ele poderá ser modificado diretamente pelo código do aplicativo. Não há necessidade de chamar SetState novamente no final (mas também sem danos). Se GetState<TState> for chamado várias vezes, o mesmo tipo deverá ser usado.

Por fim, os seguintes membros são usados para sinalizar outras entidades ou iniciar novas orquestrações:

  • SignalEntity(EntityId, operation, input): envia uma mensagem unidirecional para uma entidade.
  • CreateNewOrchestration(orchestratorFunctionName, input): inicia uma nova orquestração.
[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;
    });
}

Próximas etapas