Příručka pro vývojáře k trvalým entitě v .NET

V tomto článku podrobně popisujeme dostupná rozhraní pro vývoj trvalých entit pomocí .NET, včetně příkladů a obecných rad.

Funkce entit poskytují vývojářům bezserverových aplikací pohodlný způsob, jak uspořádat stav aplikace jako kolekci jemně odstupňovaných entit. Další podrobnosti o základních konceptech najdete v článku Durable Entities: Koncepty .

V současné době nabízíme dvě rozhraní API pro definování entit:

  • Syntaxe založená na třídách představuje entity a operace jako třídy a metody. Tato syntaxe vytváří snadno čitelný kód a umožňuje vyvolání operací způsobem kontrolovaných typem prostřednictvím rozhraní.

  • Syntaxe založená na funkcích je rozhraní nižší úrovně, které představuje entity jako funkce. Poskytuje přesnou kontrolu nad tím, jak se operace entit odesílají a jak se spravuje stav entity.

Tento článek se zaměřuje především na syntaxi založenou na třídě, protože očekáváme, že bude vhodnější pro většinu aplikací. Syntaxe založená na funkcích ale může být vhodná pro aplikace, které chtějí definovat nebo spravovat vlastní abstrakce pro stav a operace entity. Může být také vhodné pro implementaci knihoven, které vyžadují obecné typy, které syntaxe založená na třídách v současné době nepodporuje.

Poznámka:

Syntaxe založená na třídách je pouze vrstva nad syntaxí založenou na funkci, takže obě varianty lze ve stejné aplikaci zaměnitelně.

Definování tříd entit

Následující příklad je implementace Counter entity, která ukládá jednu hodnotu typu integer, a nabízí čtyři operace Add, Reset, Geta 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>();
}

Funkce Run obsahuje často používané šablony pro použití syntaxe založené na třídě. Musí to být statická funkce Azure Functions. Spustí se jednou pro každou zprávu operace, která je zpracována entitou. Když DispatchAsync<T> je volána a entita ještě není v paměti, vytvoří objekt typu T a naplní jeho pole z posledního trvalého json nalezeného v úložišti (pokud existuje). Pak vyvolá metodu s odpovídajícím názvem.

Funkce EntityTriggerRun v této ukázce nemusí být umístěná v samotné třídě Entity. Může se nacházet v libovolném platném umístění pro funkci Azure Functions: uvnitř oboru názvů nejvyšší úrovně nebo uvnitř třídy nejvyšší úrovně. Pokud je však vnořená hlouběji (například funkce je deklarována uvnitř vnořené třídy), nebude tato funkce rozpoznána nejnovějším modulem runtime.

Poznámka:

Stav entity založené na třídě se vytvoří implicitně před tím, než entita zpracuje operaci, a lze ji odstranit explicitně v operaci voláním Entity.Current.DeleteState().

Poznámka:

K spouštění entit v izolovaném modelu potřebujete verzi 4.0.5455 nástrojů Azure Functions Core Tools nebo vyšší.

Existují dva způsoby definování entity jako třídy v izolovaném pracovním modelu jazyka C#. Vytvářejí entity s různými strukturami serializace stavu.

Při definování entity se při definování entity serializuje celý objekt.

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

Implementace TaskEntity<TState>založená na ní, což usnadňuje použití injektáže závislostí. V tomto případě je stav deserializován na State vlastnost a žádná jiná vlastnost není serializována/deserializována.

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

Upozorňující

Při psaní entit, které jsou odvozeny nebo ITaskEntityTaskEntity<TState>, je důležité nenázvit metodu triggeru RunAsyncentity . To způsobí chyby za běhu při vyvolání entity, protože existuje nejednoznačná shoda s názvem metody RunAsync kvůli ITaskEntity tomu, že už definujete instanci na úrovni RunAsync.

Odstranění entit v izolovaném modelu

Odstranění entity v izolovaném modelu se provádí nastavením stavu entity na null. Způsob, jakým se to dosahuje, závisí na tom, jakou cestu implementace entity se používá.

  • Při odvození nebo použití syntaxe založené na funkcích se odstranění provádí voláním TaskEntityOperation.State.SetState(null).ITaskEntity
  • Při odvození z TaskEntity<TState>objektu delete je implicitně definováno. Lze ho však přepsat definováním metody Delete entity. Stav lze také odstranit z jakékoli operace prostřednictvím this.State = null.
    • Chcete-li odstranit nastavením stavu na hodnotu null, musí TState být nullable.
    • Implicitně definovaná operace odstranění odstraní non-nullable TState.
  • Při použití POCO jako vašeho stavu (neodvozuje z TaskEntity<TState>), odstranění je implicitně definováno. Operaci odstranění je možné přepsat definováním metody Delete pro POCO. Neexistuje však způsob, jak nastavit stav null na trasu POCO, takže implicitně definovaná operace odstranění je jediným skutečným odstraněním.

Požadavky na třídu

Třídy entit jsou objekty POCOs (prosté staré objekty CLR), které nevyžadují žádné zvláštní supertřídy, rozhraní nebo atributy. Mějte však na paměti následující:

  • Třída musí být konstruktovatelná (viz Konstrukce entity).
  • Třída musí být serializovatelná json (viz serializace entity).

Každá metoda, která má být vyvolána jako operace, musí splňovat další požadavky:

  • Operace musí mít maximálně jeden argument a nesmí mít žádné přetížení ani argumenty obecného typu.
  • Operace, která se má volat z orchestrace pomocí rozhraní, musí vrátit Task nebo Task<T>.
  • Argumenty a návratové hodnoty musí být serializovatelné hodnoty nebo objekty.

Co můžou operace dělat?

Všechny operace entit mohou číst a aktualizovat stav entity a změny stavu se automaticky uchovávají v úložišti. Kromě toho můžou operace provádět externí vstupně-výstupní operace nebo jiné výpočty v rámci obecných limitů, které jsou společné pro všechny služby Azure Functions.

Operace mají také přístup k funkcím poskytovaným kontextem Entity.Current :

  • EntityName: název aktuálně spuštěné entity.
  • EntityKey: klíč aktuálně spuštěné entity.
  • EntityId: ID aktuálně spuštěné entity (zahrnuje název a klíč).
  • SignalEntity: Odešle jednosměrnou zprávu do entity.
  • CreateNewOrchestration: spustí novou orchestraci.
  • DeleteState: Odstraní stav této entity.

Můžeme například upravit entitu čítače, aby spustila orchestraci, když čítač dosáhne 100 a předá ID entity jako vstupní argument:

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

Přímý přístup k entitě

K entitě založeným na třídách je možné přistupovat přímo pomocí explicitních názvů řetězců pro entitu a její operace. Tato část obsahuje příklady. Podrobnější vysvětlení základních konceptů (například signálů a volání) najdete v diskuzi v entitách Accessu.

Poznámka:

Pokud je to možné, měli byste přistupovat k entitám prostřednictvím rozhraní, protože poskytuje více kontroly typů.

Příklad: Entita klientských signálů

Následující funkce Azure Http implementuje operaci DELETE pomocí konvencí REST. Odešle signál pro odstranění do entity čítače, jejíž klíč se předává v cestě 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);
}

Příklad: Klient čte stav entity.

Následující funkce Azure Http implementuje operaci GET pomocí konvencí REST. Přečte aktuální stav entity čítače, jejíž klíč se předává v cestě 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);
}

Poznámka:

Objekt vrácený ReadEntityStateAsync pouze místní kopií, tj. snímek stavu entity z nějakého dřívějšího bodu v čase. Konkrétně může být zastaralá a úprava tohoto objektu nemá žádný vliv na skutečnou entitu.

Příklad: Orchestrace nejprve signalizuje volání entity

Následující orchestrace signalizuje entitu čítače, která ji zvýší, a pak zavolá stejnou entitu, aby si přečetla její nejnovější hodnotu.

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

Příklad: Entita klientských signálů

Následující funkce Azure Http implementuje operaci DELETE pomocí konvencí REST. Odešle signál pro odstranění do entity čítače, jejíž klíč se předává v cestě 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);
}

Příklad: Klient čte stav entity.

Následující funkce Azure Http implementuje operaci GET pomocí konvencí REST. Přečte aktuální stav entity čítače, jejíž klíč se předává v cestě 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;
}

Příklad: Orchestrace nejprve signalizuje volání entity

Následující orchestrace signalizuje entitu čítače, která ji zvýší, a pak zavolá stejnou entitu, aby si přečetla její nejnovější hodnotu.

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

Přístup k entitám prostřednictvím rozhraní

Rozhraní se dají použít pro přístup k entitám prostřednictvím vygenerovaných objektů proxy. Tento přístup zajišťuje, že název a typ argumentu operace odpovídají implementovanému objektu. Doporučujeme používat rozhraní pro přístup k entitám, kdykoli je to možné.

Příklad čítače můžeme například upravit následujícím způsobem:

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

public class Counter : ICounter
{
    ...
}

Třídy entit a rozhraní entit se podobají rozhraním zrn a zrn, která jsou oblíbená společností Orleans. Další informace o podobnostech a rozdílech mezi Durable Entities a Orleans naleznete v tématu Porovnání s virtuálními aktéry.

Kromě poskytování kontroly typů jsou rozhraní užitečná pro lepší oddělení obav v aplikaci. Například vzhledem k tomu, že entita může implementovat více rozhraní, může jedna entita sloužit více rolím. Vzhledem k tomu, že rozhraní může implementovat více entit, lze obecné komunikační vzory implementovat jako opakovaně použitelné knihovny.

Příklad: Entita klientských signálů prostřednictvím rozhraní

Klientský kód může použít SignalEntityAsync<TEntityInterface> k odesílání signálů entit, které implementují TEntityInterface. Příklad:

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

V tomto příkladu proxy je parametr dynamicky generovanou instancí ICounter, která interně překládá volání na Delete signál.

Poznámka:

Rozhraní SignalEntityAsync API je možné použít pouze pro jednosměrné operace. I když operace vrátí Task<T>hodnotu , hodnota parametru T bude vždy null nebo default, nikoli skutečný výsledek. Například nemá smysl signalizovat Get operaci, protože se nevrátí žádná hodnota. Místo toho můžou klienti použít buď ReadStateAsync přímý přístup ke stavu čítače, nebo mohou spustit funkci orchestrátoru Get , která tuto operaci volá.

Příklad: Orchestrace nejprve signalizuje volání entity prostřednictvím proxy serveru.

K volání nebo signalizaci entity z orchestrace CreateEntityProxy je možné použít spolu s typem rozhraní k vygenerování proxy serveru pro entitu. Tento proxy server pak lze použít k volání nebo signalizačním operacím:

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

Implicitně se všechny operace, které vrací, signalizují a všechny operace, které vrací voidTask nebo Task<T> se volají. Jedno může změnit toto výchozí chování a operace signálu, i když vrací úlohu pomocí SignalEntity<IInterfaceType> metody explicitně.

Kratší možnost pro určení cíle

Při volání nebo signalizaci entity pomocí rozhraní musí první argument zadat cílovou entitu. Cíl lze zadat buď zadáním ID entity, nebo v případech, kdy existuje jen jedna třída, která implementuje entitu, pouze klíč entity:

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

Pokud je zadán pouze klíč entity a za běhu nelze najít jedinečnou implementaci, InvalidOperationException vyvolá se.

Omezení rozhraní entit

Jako obvykle musí být všechny typy parametrů a návratových typů serializovatelné ve formátu JSON. V opačném případě jsou výjimky serializace vyvolány za běhu.

Vynucujeme také několik dalších pravidel:

  • Rozhraní entit musí být definována ve stejném sestavení jako třída entity.
  • Rozhraní entit musí definovat pouze metody.
  • Rozhraní entit nesmí obsahovat obecné parametry.
  • Metody rozhraní entity nesmí mít více než jeden parametr.
  • Metody rozhraní entity musí vracet void, Tasknebo Task<T>.

Pokud jsou některá z těchto pravidel porušena, je vyvolán za běhu, InvalidOperationException pokud je rozhraní použito jako typ argumentu , SignalEntitySignalEntityAsyncnebo CreateEntityProxy. Zpráva o výjimce vysvětluje, které pravidlo bylo přerušeno.

Poznámka:

Návratové void metody rozhraní mohou být signalizovat pouze jednosměrně, nevolané (obousměrné). Metody rozhraní vracející Task nebo Task<T> se dají volat nebo signalizovat. Pokud je volána, vrátí výsledek operace nebo znovu vyvolá výjimky vyvolané operací. Při signalizaci však nevracejí skutečný výsledek nebo výjimku z operace, ale pouze výchozí hodnotu.

Tento proces se v izolovaném pracovním procesu .NET v současné době nepodporuje.

Serializace entit

Vzhledem k tomu, že stav entity je trvale trvalý, musí být třída entity serializovatelná. Modul runtime Durable Functions používá pro tento účel knihovnu Json.NET , která podporuje zásady a atributy k řízení procesu serializace a deserializace. Nejčastěji používané datové typy jazyka C# (včetně polí a typů kolekcí) jsou již serializovatelné a lze je snadno použít k definování stavu trvalých entit.

Například Json.NET může snadno serializovat a deserializovat následující třídu:

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

    ...
}

Atributy serializace

V předchozím příkladu jsme se rozhodli zahrnout několik atributů, aby byla podkladová serializace více viditelná:

  • Třídu označíme poznámkami [JsonObject(MemberSerialization.OptIn)] , abychom si připomněli, že třída musí být serializovatelná a zachovat pouze členy, které jsou explicitně označeny jako vlastnosti JSON.
  • Pole, která se mají zachovat, [JsonProperty("name")] si připomeneme, že pole je součástí trvalého stavu entity, a zadáváme název vlastnosti, který se má použít v reprezentaci JSON.

Tyto atributy však nejsou povinné; jiné konvence nebo atributy jsou povoleny, pokud pracují s Json.NET. Můžete například použít [DataContract] atributy nebo vůbec žádné atributy:

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

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

Ve výchozím nastavení není název třídy * uložen jako součást reprezentace JSON: to znamená, že používáme TypeNameHandling.None jako výchozí nastavení. Toto výchozí chování lze přepsat pomocí JsonObject atributů nebo JsonProperty atributů.

Provádění změn definic tříd

Při provádění změn v definici třídy po spuštění aplikace je potřeba věnovat pozornost, protože uložený objekt JSON už nemůže odpovídat nové definici třídy. Přesto je často možné správně pracovat se změnou formátů dat, pokud jeden rozumí procesu deserializace používaného JsonConvert.PopulateObject.

Tady je například několik příkladů změn a jejich efektu:

  • Když se přidá nová vlastnost, která není v uloženém formátu JSON, předpokládá se její výchozí hodnota.
  • Při odebrání vlastnosti, která se nachází v uloženém formátu JSON, dojde ke ztrátě předchozího obsahu.
  • Když je vlastnost přejmenována, efekt je jako kdyby se odebral starý a přidal nový.
  • Když se typ vlastnosti změní, aby se už nedá deserializovat z uloženého JSON, vyvolá se výjimka.
  • Když se typ vlastnosti změní, ale přesto se dá deserializovat z uloženého JSON, provede to.

Pro přizpůsobení chování Json.NET je k dispozici mnoho možností. Chcete-li například vynutit výjimku, pokud uložený JSON obsahuje pole, které není přítomné ve třídě, zadejte atribut JsonObject(MissingMemberHandling = MissingMemberHandling.Error). Je také možné napsat vlastní kód pro deserializaci, který může číst JSON uložený v libovolných formátech.

Výchozí chování serializace se změnilo z Newtonsoft.Json na System.Text.Json. Další informace najdete tady.

Konstrukce entit

Někdy chceme mít větší kontrolu nad vytvářením objektů entit. Nyní popisujeme několik možností pro změnu výchozího chování při vytváření objektů entity.

Vlastní inicializace při prvním přístupu

Občas potřebujeme provést nějakou speciální inicializaci před odesláním operace do entity, která nebyla nikdy přístupná nebo která byla odstraněna. Chcete-li zadat toto chování, můžete přidat podmínku před DispatchAsync:

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

Vazby ve třídách entit

Na rozdíl od běžných funkcí nemají metody tříd entit přímý přístup ke vstupním a výstupním vazbám. Místo toho musí být data vazby zachycena v deklaraci funkce vstupního bodu a pak předána metodě DispatchAsync<T> . Všechny předané DispatchAsync<T> objekty se automaticky předají konstruktoru třídy entity jako argument.

Následující příklad ukazuje, jak CloudBlobContainer lze zpřístupnit odkaz ze vstupní vazby objektu blob pro entitu založenou na třídě.

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

Další informace o vazbách ve službě Azure Functions najdete v dokumentaci k triggerům a vazbám Azure Functions.

Injektáž závislostí ve třídách entit

Třídy entit podporují injektáž závislostí Azure Functions. Následující příklad ukazuje, jak zaregistrovat IHttpClientFactory službu do entity založené na třídě.

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

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

Následující fragment kódu ukazuje, jak začlenit vloženou službu do vaší třídy entity.

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

Vlastní inicializace při prvním přístupu

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

Vazby ve třídách entit

Následující příklad ukazuje, jak použít vstupní vazbu objektu blob v entitě založené na třídě.

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

Další informace o vazbách ve službě Azure Functions najdete v dokumentaci k triggerům a vazbám Azure Functions.

Injektáž závislostí ve třídách entit

Třídy entit podporují injektáž závislostí Azure Functions.

Následující příklad ukazuje, jak nakonfigurovat HttpClient v program.cs souboru, který se má importovat později ve třídě entity.

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

Tady je postup, jak začlenit vloženou službu do třídy entity.

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

Poznámka:

Abyste se vyhnuli problémům se serializací, nezapomeňte vyloučit pole určená k uložení vloženého hodnot ze serializace.

Poznámka:

Na rozdíl od použití injektáže konstruktoru v běžném rozhraní .NET Azure Functions musí být deklarována staticmetoda vstupního bodu funkcí pro entity založené na třídách . Deklarace vstupního bodu nestatické funkce může způsobit konflikty mezi normálním inicializátorem objektů Azure Functions a inicializátorem objektu Durable Entities.

Syntaxe založená na funkcích

Zatím jsme se zaměřili na syntaxi založenou na třídě, protože očekáváme, že bude vhodnější pro většinu aplikací. Syntaxe založená na funkcích ale může být vhodná pro aplikace, které chtějí definovat nebo spravovat vlastní abstrakce pro stav a operace entity. Může být také vhodné při implementaci knihoven, které vyžadují genericitu, která není aktuálně podporována syntaxí založenou na třídě.

Pomocí syntaxe založené na funkcích funkce funkce entita explicitně zpracovává odesílání operací a explicitně spravuje stav entity. Například následující kód ukazuje entitu Counter implementovanou pomocí syntaxe založené na funkcích.

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

Objekt kontextu entity

K funkcím specifickým pro entity lze přistupovat prostřednictvím kontextového objektu typu IDurableEntityContext. Tento kontextový objekt je k dispozici jako parametr pro funkci entity a prostřednictvím async-local vlastnosti Entity.Current.

Následující členové poskytují informace o aktuální operaci a umožňují nám zadat návratovou hodnotu.

  • EntityName: název aktuálně spuštěné entity.
  • EntityKey: klíč aktuálně spuštěné entity.
  • EntityId: ID aktuálně spuštěné entity (zahrnuje název a klíč).
  • OperationName: název aktuální operace.
  • GetInput<TInput>(): získá vstup pro aktuální operaci.
  • Return(arg): Vrátí hodnotu orchestrace, která volala operaci.

Následující členové spravují stav entity (vytvoření, čtení, aktualizace, odstranění).

  • HasState: zda entita existuje, tj. má nějaký stav.
  • GetState<TState>(): získá aktuální stav entity. Pokud ještě neexistuje, vytvoří se.
  • SetState(arg): vytvoří nebo aktualizuje stav entity.
  • DeleteState(): Odstraní stav entity, pokud existuje.

Pokud je stav vrácený objektem GetState , může ho přímo upravit kód aplikace. Na konci už nemusíte volat SetState znovu (ale ani žádné škody). Pokud GetState<TState> se volá vícekrát, musí být použit stejný typ.

Nakonec se k signalizaci jiných entit používají následující členové nebo se spustí nové orchestrace:

  • SignalEntity(EntityId, operation, input): Odešle jednosměrnou zprávu do entity.
  • CreateNewOrchestration(orchestratorFunctionName, input): spustí novou orchestraci.
[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;
    });
}

Další kroky