Condividi tramite


Guida dello sviluppatore alle entità durevoli in .NET

In questo articolo vengono descritte le interfacce disponibili per lo sviluppo di entità durevoli con .NET in dettaglio, inclusi esempi e consigli generali.

Le funzioni di entità offrono agli sviluppatori di applicazioni serverless un modo pratico per organizzare lo stato dell'applicazione come raccolta di entità con granularità fine. Per altri dettagli sui concetti sottostanti, vedere l'articolo Durable Entities: Concepts (Entità durevoli: concetti ).

Attualmente sono disponibili due API per la definizione delle entità:

  • La sintassi basata su classi rappresenta entità e operazioni come classi e metodi. Questa sintassi produce codice facilmente leggibile e consente di richiamare le operazioni in modo controllato dai tipi tramite interfacce.

  • La sintassi basata su funzione è un'interfaccia di livello inferiore che rappresenta le entità come funzioni. Fornisce un controllo preciso sulla modalità di invio delle operazioni dell'entità e sul modo in cui viene gestito lo stato dell'entità.

Questo articolo è incentrato principalmente sulla sintassi basata sulla classe, come previsto per la maggior parte delle applicazioni. Tuttavia, la sintassi basata su funzione può essere appropriata per le applicazioni che desiderano definire o gestire le proprie astrazioni per lo stato e le operazioni dell'entità. Può anche essere appropriato per l'implementazione di librerie che richiedono genericità non attualmente supportata dalla sintassi basata su classi.

Nota

La sintassi basata su classi è solo un livello sulla sintassi basata su funzione, quindi entrambe le varianti possono essere usate in modo intercambiabile nella stessa applicazione.

Definizione delle classi di entità

L'esempio seguente è un'implementazione di un'entità Counter che archivia un singolo valore di tipo integer e offre quattro operazioni 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>();
}

La Run funzione contiene il boilerplate necessario per l'uso della sintassi basata sulla classe. Deve essere una funzione statica di Azure. Viene eseguito una volta per ogni messaggio dell'operazione elaborato dall'entità. Quando DispatchAsync<T> viene chiamato e l'entità non è già in memoria, crea un oggetto di tipo T e popola i relativi campi dall'ultimo JSON persistente trovato nell'archiviazione (se presente). Richiama quindi il metodo con il nome corrispondente.

La EntityTrigger funzione, Run in questo esempio, non deve trovarsi all'interno della classe Entity stessa. Può risiedere in qualsiasi posizione valida per una funzione di Azure: all'interno dello spazio dei nomi di primo livello o all'interno di una classe di primo livello. Tuttavia, se il livello di profondità annidato (ad esempio, la funzione viene dichiarata all'interno di una classe nidificata ), questa funzione non verrà riconosciuta dal runtime più recente.

Nota

Lo stato di un'entità basata su classe viene creato in modo implicito prima che l'entità elabori un'operazione e possa essere eliminata in modo esplicito in un'operazione chiamando Entity.Current.DeleteState().

Nota

È necessario o versioni successive per eseguire entità nel modello isolato.

Esistono due modi per definire un'entità come classe nel modello di lavoro isolato C#. Producono entità con strutture di serializzazione dello stato diverse.

Con l'approccio seguente, l'intero oggetto viene serializzato durante la definizione di un'entità.

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

Implementazione TaskEntity<TState>basata su , che semplifica l'uso dell'inserimento delle dipendenze. In questo caso, lo stato viene deserializzato per la State proprietà e nessun'altra proprietà viene serializzata/deserializzata.

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

Avviso

Quando si scrivono entità che derivano da ITaskEntity o TaskEntity<TState>, è importante non denominare il metodo trigger dell'entità RunAsync. Ciò causa errori di runtime quando si richiama l'entità, perché esiste una corrispondenza ambigua con il nome del metodo "RunAsync" a causa della definizione già di un "RunAsync" a ITaskEntity livello di istanza.

Eliminazione di entità nel modello isolato

L'eliminazione di un'entità nel modello isolato viene eseguita impostando lo stato dell'entità su nulle questo processo dipende dal percorso di implementazione dell'entità usato:

  • Quando si deriva da ITaskEntity o si usa la sintassi basata su funzione, l'eliminazione viene eseguita chiamando TaskEntityOperation.State.SetState(null).
  • Quando si deriva da TaskEntity<TState>, l'eliminazione viene definita in modo implicito. Tuttavia, può essere sottoposto a override definendo un metodo Delete nell'entità. Lo stato può anche essere eliminato da qualsiasi operazione tramite this.State = null.
    • Per eliminare impostando lo stato su Null, è necessario TState che sia nullable.
    • L'operazione di eliminazione definita in modo implicito elimina TState.
  • Quando si usa un POCO come stato (non derivando da TaskEntity<TState>), l'eliminazione viene definita in modo implicito. È possibile eseguire l'override dell'operazione di eliminazione definendo un metodo Delete in POCO. Tuttavia, non esiste un modo per impostare lo stato su null nella route POCO, quindi l'operazione di eliminazione definita in modo implicito è l'unica vera eliminazione.

Requisiti di classe

Le classi di entità sono oggetti POCO (semplici oggetti CLR precedenti) che non richiedono superclassi speciali, interfacce o attributi. Tuttavia:

  • La classe deve essere costruttibile (vedere Costruzione di entità).
  • La classe deve essere serializzabile in JSON (vedere Serializzazione di entità).

Inoltre, qualsiasi metodo richiamato come operazione deve soddisfare altri requisiti:

  • Un'operazione deve avere al massimo un argomento, ma non deve avere overload o argomenti di tipo generico.
  • Un'operazione destinata a essere chiamata da un'orchestrazione tramite un'interfaccia deve restituire Task o Task<T>.
  • Gli argomenti e i valori restituiti devono essere valori serializzabili o oggetti.

Che cosa possono fare le operazioni?

Tutte le operazioni di entità possono leggere e aggiornare lo stato dell'entità e le modifiche apportate allo stato vengono automaticamente rese persistenti nell'archiviazione. Inoltre, le operazioni possono eseguire operazioni di I/O esterne o altri calcoli, entro i limiti generali comuni a tutti i Funzioni di Azure.

Le operazioni hanno anche accesso alle funzionalità fornite dal Entity.Current contesto:

  • EntityName: nome dell'entità attualmente in esecuzione.
  • EntityKey: chiave dell'entità attualmente in esecuzione.
  • EntityId: ID dell'entità attualmente in esecuzione (include nome e chiave).
  • SignalEntity: invia un messaggio unidirezionale a un'entità.
  • CreateNewOrchestration: avvia una nuova orchestrazione.
  • DeleteState: elimina lo stato di questa entità.

Ad esempio, è possibile modificare l'entità contatore in modo che avvii un'orchestrazione quando il contatore raggiunge 100 e passi l'ID entità come argomento di input:

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

Accesso diretto alle entità

È possibile accedere direttamente alle entità basate su classi, usando nomi di stringa espliciti per l'entità e le relative operazioni. In questa sezione vengono forniti esempi. Per una spiegazione più approfondita dei concetti sottostanti (ad esempio segnali e chiamate), vedere la discussione in Entità di Access.

Nota

Se possibile, è consigliabile accedere alle entità tramite interfacce, perché offre un controllo dei tipi maggiore.

Esempio: entità segnali client

La funzione HTTP di Azure seguente implementa un'operazione DELETE usando le convenzioni REST. Invia un segnale di eliminazione all'entità contatore la cui chiave viene passata nel percorso 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);
}

Esempio: client legge lo stato dell'entità

La funzione HTTP di Azure seguente implementa un'operazione GET usando le convenzioni REST. Legge lo stato corrente dell'entità contatore la cui chiave viene passata nel percorso 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

L'oggetto restituito da ReadEntityStateAsync è solo una copia locale, ovvero uno snapshot dello stato dell'entità da un momento precedente. In particolare, può essere obsoleto e la modifica di questo oggetto non ha alcun effetto sull'entità effettiva.

Esempio: L'orchestrazione segnala prima, poi chiama l'entità

L'orchestrazione seguente segnala a un'entità contatore di incrementarla e quindi chiama la stessa entità per leggere il valore più 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;
}

Esempio: entità segnali client

La funzione HTTP di Azure seguente implementa un'operazione DELETE usando le convenzioni REST. Invia un segnale di eliminazione all'entità contatore la cui chiave viene passata nel percorso 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);
}

Esempio: client legge lo stato dell'entità

La funzione HTTP di Azure seguente implementa un'operazione GET usando le convenzioni REST. Legge lo stato corrente dell'entità contatore la cui chiave viene passata nel percorso 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;
}

Esempio: L'orchestrazione segnala prima, poi chiama l'entità

L'orchestrazione seguente segnala a un'entità contatore di incrementarla e quindi chiama la stessa entità per leggere il valore più 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; 
}

Accesso alle entità tramite interfacce

Le interfacce possono essere usate per accedere alle entità tramite oggetti proxy generati. Questo approccio garantisce che il nome e il tipo di argomento di un'operazione corrispondano a quanto implementato. È consigliabile usare le interfacce quando possibile per accedere alle entità.

Ad esempio, è possibile modificare l'esempio di contatore:

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

public class Counter : ICounter
{
    ...
}

Le classi di entità e le interfacce di entità sono simili alle interfacce granulari e granulari diffuse da Orleans. Per altre informazioni sulle analogie e sulle differenze tra Durable Entities e Orleans, vedere Confronto con attori virtuali.

Oltre a fornire il controllo dei tipi, le interfacce sono utili per una migliore separazione delle problematiche all'interno dell'applicazione. Ad esempio, poiché un'entità può implementare più interfacce, una singola entità può gestire più ruoli. Inoltre, poiché più entità possono implementare un'interfaccia, i modelli di comunicazione generali possono essere implementati come librerie riutilizzabili.

Esempio: l'entità segnala il client tramite l'interfaccia

Il codice client può usare SignalEntityAsync<TEntityInterface> per inviare segnali alle entità che implementano TEntityInterface. Ad esempio:

[FunctionName("DeleteCounter")]
public static async Task<HttpResponseMessage> DeleteCounter(
    [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Counter/{entityKey}")] HttpRequestMessage req,
    [DurableClient] IDurableEntityClient client,
    string entityKey)
{
    var entityId = new EntityId("Counter", entityKey);
    await client.SignalEntityAsync<ICounter>(entityId, proxy => proxy.Delete());    
    return req.CreateResponse(HttpStatusCode.Accepted);
}

In questo esempio, il proxy parametro è un'istanza generata dinamicamente di ICounter, che converte internamente la chiamata a Delete in un segnale.

Nota

Le SignalEntityAsync API possono essere usate solo per le operazioni unidirezionali. Anche se un'operazione restituisce Task<T>, il valore del T parametro è sempre Null o default anziché il risultato effettivo. Ad esempio, non ha senso segnalare l'operazione Get perché non restituisce un valore. I client possono invece usare ReadStateAsync per accedere direttamente allo stato del contatore o avviare una funzione dell'agente di orchestrazione che chiama l'operazione Get .

Esempio: L'orchestrazione prima segnala e poi chiama l'entità tramite un proxy

Per chiamare o segnalare un'entità dall'interno di un'orchestrazione, CreateEntityProxy è possibile usare, insieme al tipo di interfaccia, per generare un proxy per l'entità. Questo proxy può quindi essere usato per chiamare o segnalare operazioni:

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

In modo implicito, tutte le operazioni restituite vengono segnalate e tutte le operazioni che restituiscono voidTask o Task<T> vengono chiamate. È possibile modificare questo comportamento predefinito e segnalare le operazioni anche se restituiscono Task usando il SignalEntity<IInterfaceType> metodo in modo esplicito.

Opzione più breve per specificare la destinazione

Quando si chiama o si segnala un'entità usando un'interfaccia, il primo argomento deve specificare l'entità di destinazione. La destinazione può essere specificata definendo l'ID entità o semplicemente la chiave di entità quando è presente una sola classe che implementa l'entità:

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

Se viene specificata solo la chiave di entità e non è possibile trovare un'implementazione univoca in fase di esecuzione, InvalidOperationException viene generata un'eccezione.

Restrizioni sulle interfacce di entità

Come di consueto, tutti i tipi di parametro e restituiti devono essere serializzabili in JSON. In caso contrario, le eccezioni di serializzazione vengono generate in fase di esecuzione. Si applicano anche le regole seguenti:

  • Le interfacce di entità devono essere definite nello stesso assembly della classe di entità.
  • Le interfacce di entità devono definire solo i metodi.
  • Le interfacce di entità non devono contenere parametri generici.
  • I metodi dell'interfaccia entità non devono avere più di un parametro.
  • I metodi dell'interfaccia di entità devono restituire void, Tasko Task<T>.

Se una di queste regole viene violata, viene generata un'eccezione InvalidOperationException in fase di esecuzione quando l'interfaccia viene usata come argomento di tipo per SignalEntity, SignalEntityAsynco CreateEntityProxy. Il messaggio di eccezione spiega quale regola è stata interrotta.

Nota

I metodi di interfaccia che void restituiscono possono essere indicati solo (unidirezionale), non chiamati (bidirezionali). I metodi di interfaccia che restituiscono Task o Task<T> possono essere chiamati o segnalati. Se vengono chiamati, restituiscono il risultato dell'operazione o rilanciano eccezioni sollevate dall'operazione. Se segnalato, restituiscono il valore predefinito e non il risultato effettivo o l'eccezione dall'operazione.

Questo non è attualmente supportato nel worker isolato di .NET.

Serializzazione delle entità

Poiché lo stato di un'entità è persistente in modo permanente, la classe di entità deve essere serializzabile. Il runtime di Durable Functions usa la libreria Json.NET a questo scopo, che supporta criteri e attributi per controllare il processo di serializzazione e deserializzazione. I tipi di dati C# più comunemente usati (inclusi matrici e tipi di raccolta) sono già serializzabili e possono essere usati facilmente per definire lo stato delle entità durevoli.

Ad esempio, Json.NET può serializzare e deserializzare facilmente la classe seguente:

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

    ...
}

Attributi di serializzazione

Nell'esempio precedente sono inclusi diversi attributi per rendere più visibile la serializzazione sottostante:

  • La classe viene annotata con [JsonObject(MemberSerialization.OptIn)] per ricordare che la classe deve essere serializzabile e per rendere persistenti solo i membri contrassegnati in modo esplicito come proprietà JSON.
  • I campi da rendere persistenti vengono annotati per [JsonProperty("name")] ricordare che un campo fa parte dello stato dell'entità persistente e per specificare il nome della proprietà da usare nella rappresentazione JSON.

Tuttavia, questi attributi non sono obbligatori; altre convenzioni o attributi sono consentiti purché funzionino con Json.NET. Ad esempio, è possibile usare [DataContract] attributi o nessun attributo:

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

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

Per impostazione predefinita, il nome della classe non viene archiviato come parte della rappresentazione JSON, ovvero viene usata TypeNameHandling.None come impostazione predefinita. Questo comportamento predefinito può essere sottoposto a override tramite JsonObject attributi o JsonProperty .

Apportare modifiche alle definizioni di classe

Quando si apportano modifiche a una definizione di classe dopo l'esecuzione di un'applicazione, è necessario prestare attenzione perché l'oggetto JSON archiviato non può più corrispondere alla nuova definizione di classe. Spesso è possibile gestire correttamente la modifica dei formati di dati purché si comprenda il processo di deserializzazione usato da JsonConvert.PopulateObject. Di seguito sono riportati alcuni esempi di modifiche e il relativo impatto:

  • Quando viene aggiunta una nuova proprietà non presente nel codice JSON archiviato, presuppone il valore predefinito.
  • Quando viene rimossa una proprietà presente nel codice JSON archiviato, il contenuto precedente viene perso.
  • Quando una proprietà viene rinominata, l'effetto è quello di rimuovere quello precedente e aggiungerne uno nuovo.
  • Quando un tipo di proprietà viene modificato in modo che non possa essere deserializzato dal codice JSON archiviato, viene generata un'eccezione.
  • Quando un tipo di proprietà viene modificato in modo che possa comunque essere deserializzato dal codice JSON archiviato, lo fa.

Sono disponibili molte opzioni per personalizzare il comportamento di Json.NET. Ad esempio, per forzare un'eccezione se il codice JSON archiviato contiene un campo che non è presente nella classe , specificare l'attributo JsonObject(MissingMemberHandling = MissingMemberHandling.Error). È anche possibile scrivere codice personalizzato per la deserializzazione in grado di leggere JSON archiviato in formati arbitrari.

Il comportamento predefinito della serializzazione è cambiato da Newtonsoft.Json a System.Text.Json. Per altre informazioni, vedere Personalizzazione della serializzazione e deserializzazione.

Costruzione di entità

A volte si vuole esercitare un maggiore controllo sulla modalità di costruzione degli oggetti entità. Vengono ora descritte diverse opzioni per modificare il comportamento predefinito durante la costruzione di oggetti entità.

Inizializzazione personalizzata al primo accesso

In alcuni casi è necessario eseguire un'inizializzazione speciale prima di inviare un'operazione a un'entità a cui non è mai stato eseguito l'accesso o che è stata eliminata. Per specificare questo comportamento, è possibile aggiungere un'istruzione condizionale prima di DispatchAsync:

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

Associazioni nelle classi di entità

A differenza delle normali funzioni, i metodi della classe di entità non hanno accesso diretto alle associazioni di input e output. Al contrario, i dati di associazione devono essere acquisiti nella dichiarazione della funzione del punto di ingresso e quindi passati al metodo DispatchAsync<T>. Tutti gli oggetti passati a DispatchAsync<T> vengono passati automaticamente al costruttore della classe di entità come argomento.

L'esempio seguente illustra come è possibile rendere disponibile un riferimento a CloudBlobContainer dall'associazione di input del BLOB a un'entità basata su 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);
    }
}

Per ulteriori informazioni sulle associazioni in Funzioni di Azure, vedere i concetti di trigger e associazioni di Funzioni di Azure.

Inserimento delle dipendenze nelle classi di entità

Le classi di entità supportano l'inserimento di dipendenze di Funzioni di Azure. L'esempio seguente illustra come registrare un servizio IHttpClientFactory in un'entità basata su classe.

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

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

Il frammento di codice seguente illustra come incorporare il servizio inserito nella classe di entità:

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

Inizializzazione personalizzata al primo accesso

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

Associazioni nelle classi di entità

L'esempio seguente illustra come usare un'associazione di input BLOB in un'entità basata su classi.

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

Per altre informazioni sulle associazioni in Funzioni di Azure, vedere la documentazione su trigger e associazioni di Funzioni di Azure.

Inserimento delle dipendenze nelle classi di entità

Le classi di entità supportano l'inserimento di dipendenze di Funzioni di Azure.

Nell'esempio seguente viene illustrato come configurare un oggetto HttpClient nel program.cs file da importare in un secondo momento nella classe di entità:

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

L'esempio seguente illustra come incorporare il servizio inserito nella classe di entità:

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

Per evitare problemi di serializzazione, assicurarsi di escludere i campi che archiviano i valori inseriti dalla serializzazione.

Nota

Diversamente da quando si usa l'inserimento del costruttore nelle normali funzioni di Azure per .NET, il metodo del punto di ingresso delle funzioni per le entità basate su classi deve essere dichiarato come static. La dichiarazione di un punto di ingresso di funzione che non è statica può causare conflitti tra il normale inizializzatore di oggetti di Funzioni di Azure e l'inizializzatore di oggetti Durable Entities.

Sintassi basata su funzione

Finora ci siamo concentrati sulla sintassi basata su classi, come ci aspettiamo che sia più adatto per la maggior parte delle applicazioni. Tuttavia, la sintassi basata su funzione può essere appropriata per le applicazioni che desiderano definire o gestire le proprie astrazioni per lo stato e le operazioni dell'entità. Inoltre, può essere appropriato quando si implementano librerie che richiedono genericità non attualmente supportata dalla sintassi basata sulla classe.

Con la sintassi basata su funzione, la funzione di entità gestisce in modo esplicito l'invio dell'operazione e gestisce in modo esplicito lo stato dell'entità. Ad esempio, il codice seguente mostra l'entità Counter implementata usando la sintassi basata su funzione.

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

Oggetto contesto dell'entità

È possibile accedere alle funzionalità specifiche dell'entità tramite un oggetto contesto di tipo IDurableEntityContext. Questo oggetto di contesto è disponibile come parametro per la funzione di entità e tramite la proprietà Entity.Currentasync-local .

I membri seguenti forniscono informazioni sull'operazione corrente e consentono di specificare un valore restituito:

  • EntityName: nome dell'entità attualmente in esecuzione
  • EntityKey: chiave dell'entità attualmente in esecuzione
  • EntityId: ID dell'entità attualmente in esecuzione (include nome e chiave)
  • OperationName: nome dell'operazione corrente
  • GetInput<TInput>(): ottiene l'input per l'operazione corrente
  • Return(arg): restituisce un valore all'orchestrazione che ha chiamato l'operazione

I membri seguenti gestiscono lo stato dell'entità (creare, leggere, aggiornare, eliminare):

  • HasState: se l'entità esiste; ad esempio, ha uno stato
  • GetState<TState>(): ottiene lo stato corrente dell'entità e ne crea uno se non esiste
  • SetState(arg): crea o aggiorna lo stato dell'entità
  • DeleteState(): cancella lo stato dell'entità se esiste

Se lo stato restituito da GetState è un oggetto , il codice dell'applicazione può modificarlo. Non c'è bisogno di chiamare SetState di nuovo alla fine (ma anche nessun danno). Se GetState<TState> viene chiamato più volte, è necessario usare lo stesso tipo.

Infine, i membri seguenti segnalano altre entità o avviano nuove orchestrazioni:

  • SignalEntity(EntityId, operation, input): invia un messaggio unidirezionale a un'entità
  • CreateNewOrchestration(orchestratorFunctionName, input): avvia una nuova orchestrazione
[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;
    });
}

Passaggi successivi