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 eliminato in modo esplicito in un'operazione chiamando Entity.Current.DeleteState().

Nota

È necessario Funzioni di Azure versione 4.0.5455 di Core Tools 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 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>();
}

Avviso

Quando si scrivono entità che derivano da ITaskEntity o TaskEntity<TState>, è importante non denominare il metodo RunAsynctrigger dell'entità . Ciò causerà 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 ITaskEntity un livello di istanza "RunAsync".

Eliminazione di entità nel modello isolato

L'eliminazione di un'entità nel modello isolato viene eseguita impostando lo stato dell'entità su null. La modalità di esecuzione dipende dal percorso di implementazione dell'entità in uso.

  • 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 è possibile impostare lo stato su null nella route POCO in modo che l'operazione di eliminazione definita in modo implicito sia l'unica vera eliminazione.

Requisiti di classe

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

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

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

  • Un'operazione deve avere al massimo un argomento e non deve contenere 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: orchestrazione dei primi segnali, quindi 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: orchestrazione dei primi segnali, quindi 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 per accedere alle entità quando possibile.

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

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é un'interfaccia può essere implementata da più entità, 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 sarà sempre Null o default, non il risultato effettivo. Ad esempio, non ha senso segnalare l'operazione Get , perché non viene restituito alcun valore. I client possono invece usare ReadStateAsync per accedere direttamente allo stato del contatore oppure avviare una funzione dell'agente di orchestrazione che chiama l'operazione Get .

Esempio: orchestrazione dei primi segnali, quindi chiama l'entità tramite 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 void Task 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 specificando l'ID entità oppure, nei casi in cui è presente una sola classe che implementa l'entità, solo la chiave di 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.

Vengono applicate anche altre regole:

  • 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 Task restituiscono o Task<T> possono essere chiamati o segnalato. Se viene chiamato, restituiscono il risultato dell'operazione o generano nuovamente eccezioni generate dall'operazione. Tuttavia, quando segnalato, non restituiscono il risultato effettivo o l'eccezione dall'operazione, ma solo il valore predefinito.

Questo non è attualmente supportato nel ruolo di lavoro isolato .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 è stato scelto di includere 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. Tuttavia, 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 effetto:

  • Quando viene aggiunta una nuova proprietà, che non è presente nel codice JSON archiviato, presuppone il valore predefinito.
  • Quando una proprietà viene rimossa, che è presente nel codice JSON archiviato, il contenuto precedente viene perso.
  • Quando una proprietà viene rinominata, l'effetto è come se rimuovesse quello precedente e ne aggiungesse uno nuovo.
  • Quando il tipo di una proprietà viene modificato in modo che non possa più essere deserializzato dal codice JSON archiviato, viene generata un'eccezione.
  • Quando il tipo di una proprietà viene modificato, ma può 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 ulteriori informazioni, vedi qui.

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 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. 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.

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

Ecco 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 destinati all'archiviazione dei 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 una funzione non statica può causare conflitti tra il normale inizializzatore di oggetti 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, ovvero ha uno stato.
  • GetState<TState>(): ottiene lo stato corrente dell'entità. Se non esiste già, viene creato.
  • SetState(arg): crea o aggiorna lo stato dell'entità.
  • DeleteState(): elimina lo stato dell'entità, se esistente.

Se lo stato restituito da GetState è un oggetto , può essere modificato direttamente dal codice dell'applicazione. 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 vengono usati per segnalare altre entità o avviare 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