Guia do desenvolvedor para entidades duráveis no .NET

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

As funções de entidade fornecem aos desenvolvedores de aplicativos sem servidor uma maneira conveniente de organizar o estado do aplicativo como uma coleção de entidades refinadas. Para obter mais detalhes sobre os conceitos subjacentes, consulte o 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 código facilmente legível e permite que as operações sejam invocadas de maneira verificada por meio de interfaces.

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

Este artigo se concentra principalmente na sintaxe baseada em classe, pois esperamos que ela seja mais adequada para a maioria dos aplicativos. No entanto, a sintaxe baseada em função pode ser apropriada para aplicativos que desejam definir ou gerenciar suas próprias abstrações para o estado e as operações da entidade. Além disso, pode ser apropriado para implementar bibliotecas que exigem genericidade não suportada atualmente pela sintaxe baseada em classe.

Nota

A sintaxe baseada em classe é apenas uma camada sobre a sintaxe baseada em função, portanto, ambas as variantes podem ser usadas indistintamente no mesmo aplicativo.

Definição de classes de entidade

O exemplo a seguir é uma implementação de uma Counter entidade que armazena um único valor do tipo inteiro e oferece quatro operações Add, , Reset, Gete 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 Run função contém o clichê necessário para usar a sintaxe baseada em classe. Deve ser uma Função do Azure estática . Ele é executado uma vez para cada mensagem de operação que é 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 seus campos a partir do último JSON persistente encontrado no armazenamento (se houver). Em seguida, ele invoca o método com o nome correspondente.

A EntityTrigger Função, neste exemplo, Run não precisa residir dentro da própria classe Entity. Ele pode residir 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 tempo de execução mais recente.

Nota

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

Nota

Você precisa da versão das Ferramentas Principais do Azure Functions ou 4.0.5455 superior para executar entidades no modelo isolado.

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

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 TaskEntity<TState>implementação baseada em , que facilita o uso da injeção de dependência. Nesse caso, o estado é desserializado para a State propriedade e nenhuma outra propriedade é serializada/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 ou TaskEntity<TState>, é importante não nomear seu método RunAsyncde gatilho de ITaskEntity entidade. Isso causará erros de tempo de execução ao invocar a entidade, pois há uma correspondência ambígua com o nome do método "RunAsync" devido a ITaskEntity já definir um nível de instância "RunAsync".

Excluindo 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 da entidade está sendo usado.

  • Ao derivar ITaskEntity ou usar sintaxe baseada em função, a exclusão é realizada chamando TaskEntityOperation.State.SetState(null).
  • Quando deriva de TaskEntity<TState>, delete é implicitamente definido. No entanto, ele pode ser substituído pela definição de um método Delete na entidade. O estado também pode ser excluído de qualquer operação via this.State = null.
    • Para excluir definindo state como null é necessário TState ser nullable.
    • A operação de exclusão implicitamente definida exclui não anulável TState.
  • Ao usar um POCO como seu estado (não derivado de TaskEntity<TState>), excluir é implicitamente definido. É possível substituir a operação delete definindo um método Delete no POCO. No entanto, não há como definir o estado para null na rota POCO, portanto, a operação de exclusão implicitamente definida é a única exclusão verdadeira.

Requisitos da classe

As classes de entidade são POCOs (objetos CLR antigos simples) que não exigem superclasses, interfaces ou atributos especiais. No entanto:

Além disso, qualquer método que se destine a ser invocado como uma operação deve satisfazer outros requisitos:

  • Uma operação deve ter no máximo um argumento e não deve ter sobrecargas ou argumentos de tipo genéricos.
  • Uma operação destinada a ser chamada a partir de uma orquestração usando uma interface deve retornar Task ou Task<T>.
  • Argumentos e valores de retorno devem ser valores ou objetos serializáveis.

O que as operações podem fazer?

Todas as operações da entidade podem ler e atualizar o estado da entidade, e as alterações no estado são automaticamente persistidas 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 Funções do Azure.

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

  • EntityName: o nome da entidade atualmente em execução.
  • EntityKey: a chave da entidade atualmente em execução.
  • EntityId: o ID da entidade atualmente em execução (inclui nome e 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 passar o 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;      
}

Acesso direto a entidades

As entidades baseadas em classe podem ser acessadas diretamente, usando nomes de cadeia de caracteres explícitos para a entidade e suas operações. Esta secção fornece exemplos. Para obter uma explicação mais profunda dos conceitos subjacentes (como sinais vs. chamadas), consulte a discussão em Entidades do Access.

Nota

Sempre que possível, você deve acessar entidades por meio de interfaces, pois fornece mais verificação de tipo.

Exemplo: entidade de sinais do cliente

A seguinte função http do Azure implementa uma operação DELETE usando convenções REST. Ele 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ê o estado da entidade

A seguinte Função Http do Azure implementa uma operação GET usando convenções REST. Ele 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);
}

Nota

O objeto retornado por ReadEntityStateAsync é apenas uma cópia local, ou seja, um instantâneo do estado da entidade de algum ponto anterior no tempo. Em particular, pode ser obsoleto, e modificar este objeto não tem efeito sobre a entidade real.

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

A orquestração a seguir sinaliza uma entidade contadora para incrementá-la e, em seguida, 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: entidade de sinais do cliente

A seguinte função http do Azure implementa uma operação DELETE usando convenções REST. Ele 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ê o estado da entidade

A seguinte Função Http do Azure implementa uma operação GET usando convenções REST. Ele 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: orquestração primeiro sinaliza e depois chama entidade

A orquestração a seguir sinaliza uma entidade contadora para incrementá-la e, em seguida, 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; 
}

Acesso a entidades através de interfaces

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

Por exemplo, podemos modificar o contraexemplo da seguinte forma:

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

public class Counter : ICounter
{
    ...
}

Classes de entidade e interfaces de entidade são semelhantes aos grãos e interfaces de grãos popularizados por Orleans. Para obter mais informações sobre semelhanças e diferenças entre entidades duráveis e Orleans, consulte Comparação com atores virtuais.

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

Exemplo: cliente sinaliza entidade através da interface

O código do cliente pode ser usado SignalEntityAsync<TEntityInterface> para enviar sinais para entidades que implementam TEntityInterfaceo . 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 proxy parâmetro é uma instância gerada dinamicamente de ICounter, que converte internamente a chamada em Delete um sinal.

Nota

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

Exemplo: orquestração primeiro sinaliza e depois chama entidade através de proxy

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

[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 são sinalizadas e todas as operações que retornam voidTask ou Task<T> são chamadas. Pode-se alterar esse comportamento padrão e sinalizar operações mesmo que retornem Task, usando o SignalEntity<IInterfaceType> método explicitamente.

Opção mais curta para especificar o destino

Ao chamar ou sinalizar uma entidade usando uma interface, o primeiro argumento deve especificar a entidade de destino. O destino pode ser especificado especificando o ID da entidade ou, nos casos em que há apenas uma classe que implementa a entidade, apenas a chave da 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 às interfaces de entidade

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

Também aplicamos mais algumas regras:

  • As interfaces de entidade devem ser definidas no mesmo assembly que a classe de entidade.
  • As interfaces de entidade devem apenas definir 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 devem retornar void, Taskou Task<T>.

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

Nota

Os métodos de interface que retornam void só podem ser sinalizados (unidirecional), não chamados (bidirecional). Os métodos de interface retornam Task ou podem ser chamados ou Task<T> sinalizados. Se chamados, eles retornam o resultado da operação ou relançam exceções lançadas pela operação. No entanto, quando sinalizados, eles não retornam o resultado real ou exceção da operação, mas apenas o valor padrão.

No momento, isso não é suportado no trabalhador isolado do .NET.

Serialização de entidades

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

Por exemplo, Json.NET pode facilmente serializar e desserializar 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 que a classe deve ser serializável e para persistir apenas os membros que são explicitamente marcados como propriedades JSON.
  • Anotamos os campos a [JsonProperty("name")] serem persistidos para nos lembrar de que um campo faz parte do estado da entidade persistente e para especificar o nome da propriedade a ser usada na representação JSON.

No entanto, esses atributos não são necessários; outras convenções ou atributos são permitidos, desde que trabalhem com Json.NET. Por exemplo, pode-se usar [DataContract] atributos, 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 JsonObject ou JsonProperty atributos.

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

Alguns cuidados são necessários ao fazer alterações em uma definição de classe após a execução de um aplicativo, porque o objeto JSON armazenado não pode mais corresponder à nova definição de classe. Ainda assim, muitas vezes é possível lidar corretamente com a mudança de formatos de dados, desde que se compreenda o processo de desserialização usado pela JsonConvert.PopulateObject.

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

  • Quando uma nova propriedade é adicionada, que não está presente no JSON armazenado, ela assume seu valor padrão.
  • Quando uma propriedade é removida, que está presente no JSON armazenado, o conteúdo anterior é perdido.
  • Quando uma propriedade é renomeada, o efeito é como se removesse a antiga e adicionasse uma nova.
  • Quando o tipo de uma propriedade é alterado para que não possa mais ser desserializado do JSON armazenado, uma exceção é 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 obter mais informações, veja aqui.

Construção de entidades

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

Inicialização personalizada no primeiro acesso

Ocasionalmente, precisamos executar alguma inicialização especial antes de despachar uma operação para uma entidade que nunca foi acessada ou que foi excluída. Para especificar esse comportamento, pode-se adicionar uma condicional antes do DispatchAsync:

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

Ligaçõ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 às ligações de entrada e saída. Em vez disso, os dados de ligação devem ser capturados na declaração de função de ponto de entrada e, em seguida, passados para o DispatchAsync<T> método. Todos os objetos passados para são passados automaticamente para DispatchAsync<T> o construtor da classe de entidade como um argumento.

O exemplo a seguir mostra como uma referência da associação de entrada de blob pode ser disponibilizada para uma CloudBlobContainer 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, consulte a documentação de Gatilhos e Ligaçõ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 IHttpClientFactory serviço 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 trecho a seguir demonstra como incorporar o serviço injetado em 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;
    }
}

Ligaçõ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, consulte a documentação de Gatilhos e Ligaçõ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 seguinte demonstra como configurar um HttpClient no arquivo a ser importado program.cs 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>();
}

Nota

Para evitar problemas com a serialização, certifique-se de excluir campos destinados a armazenar valores injetados da serialização.

Nota

Ao contrário do uso da injeção de construtor no .NET Azure Functions 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 Entidades Duráveis.

Sintaxe baseada em função

Até agora, nos concentramos na sintaxe baseada em classe, pois esperamos que ela seja mais adequada para a maioria dos aplicativos. No entanto, a sintaxe baseada em função pode ser apropriada para aplicativos que desejam definir ou gerenciar suas próprias abstrações para o estado e as operações da entidade. Além disso, pode ser apropriado ao implementar bibliotecas que exigem genericidade não suportada atualmente pela sintaxe baseada em classe.

Com a sintaxe baseada em função, a Função de Entidade manipula explicitamente o despacho da operação e gerencia explicitamente o estado da entidade. Por exemplo, o código a seguir mostra a entidade Counter implementada usando a 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 da entidade

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

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 atualmente em execução.
  • EntityKey: a chave da entidade atualmente em execução.
  • EntityId: o ID da entidade atualmente em execução (inclui nome e chave).
  • OperationName: o nome da operação atual.
  • GetInput<TInput>(): obtém a entrada para a 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, tem algum estado.
  • GetState<TState>(): obtém o estado atual da entidade. Se ainda não existe, é criado.
  • SetState(arg): cria ou atualiza o estado da entidade.
  • DeleteState(): exclui o estado da entidade, se existir.

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

Finalmente, 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óximos passos