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

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í prostřednictvím rozhraní se kontrolou typu.

  • 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 entit. Může být také vhodné pro implementaci knihoven, které v současné době nepodporují syntaxi založenou na třídách.

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 používat 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é funkce pro použití syntaxe založené na třídě. Musí to být statická funkce Azure Functions. Provede 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í svá 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.

Poznámka

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

Požadavky na třídu

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

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 můžou číst a aktualizovat stav entity a změny stavu se automaticky zachovají do úložiště. Kromě toho můžou operace provádět externí vstupně-výstupní operace nebo jiné výpočty v rámci obecných limitů společných 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ě spouštěné entity.
  • EntityKey: klíč aktuálně spuštěné entity.
  • EntityId: ID aktuálně spouštěné entity (včetně názvu a klíče).
  • SignalEntity: Odešle jednosměrnou zprávu entitě.
  • CreateNewOrchestration: spustí novou orchestraci.
  • DeleteState: odstraní stav této entity.

Můžeme například upravit entitu čítače tak, 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. Níže uvádíme několik příkladů; 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é, doporučujeme přistupovat k entitám prostřednictvím rozhraní, protože poskytuje více kontroly typů.

Příklad: Entita signálů klienta

Následující funkce Azure Http implementuje operaci DELETE pomocí konvencí REST. Odešle signál delete entitě čí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 je pouze místní kopie, 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 první signály a volání entity

Následující orchestrace signalizuje entitu čítače, která ji zvýší, a pak zavolá stejnou entitu, aby 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řístup k entitám prostřednictvím rozhraní

Rozhraní lze použít pro přístup k entitám prostřednictvím vygenerovaných objektů proxy serveru. Tento přístup zajišťuje, že název a typ argumentu operace odpovídají tomu, co je implementováno. Pokud je to možné, doporučujeme používat rozhraní pro přístup k entitám.

Příklad čítače můžeme například upravit takto:

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

public class Counter : ICounter
{
    ...
}

Třídy entit a rozhraní entit jsou podobné zrna a grain rozhraní popularizované v 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 obsluhovat více rolí. Vzhledem k tomu, že rozhraní může být implementováno několika entitami, lze obecné komunikační vzory implementovat jako opakovaně použitelné knihovny.

Příklad: Klient signalizuje entitu 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 vygenerovanou instancí ICounter, která interně překládá volání do Delete signálu.

Poznámka

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

Příklad: Orchestrace nejprve signalizuje a pak volá entitu prostřednictvím proxy serveru.

Pokud chcete volat nebo signalizovat entitu z orchestrace, CreateEntityProxy můžete ji použít spolu s typem rozhraní k vygenerování proxy serveru pro entitu. Tento proxy server se pak dá 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é Task vrací void nebo Task<T> se volají. Toto výchozí chování můžete změnit a operace signálu i v případě, že vracejí úlohu, pomocí SignalEntity<IInterfaceType> metody explicitně.

Kratší možnost pro zadání 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 pouze 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 jedinečná implementace se nedá najít za běhu, 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án za běhu.

Vynucujeme také některá další pravidla:

  • Rozhraní entit musí být definována ve stejném sestavení jako třída entity.
  • Rozhraní entit musí definovat pouze metody.
  • Rozhraní entity 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 dojde k porušení některého z těchto pravidel, vyvolá InvalidOperationException se při běhu při použití rozhraní jako argument typu , SignalEntitySignalEntityAsyncnebo CreateEntityProxy. Zpráva o výjimce vysvětluje, které pravidlo bylo přerušeno.

Poznámka

Metody rozhraní vracející void se dají signalizovat (jednosměrně), nevolat (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í. Když se však signalizují, nevrací skutečný výsledek nebo výjimku z operace, ale pouze výchozí hodnota.

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 řadu zásad a atributů 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říkladu výše jsme se rozhodli zahrnout několik atributů, aby byla podkladová serializace viditelná:

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

Tyto atributy ale nejsou povinné; jiné konvence nebo atributy jsou povoleny, pokud pracují s Json.NET. Jeden může například používat [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í se název třídy neukládá jako součást reprezentace JSON: to znamená, že používáme TypeNameHandling.None jako výchozí nastavení. Toto výchozí chování může být přepsáno pomocí JsonObject atributů nebo JsonProperty atributů.

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

Při provádění změn definice třídy po spuštění aplikace se vyžaduje určitá péče, protože uložený objekt JSON už nemusí odpovídat definici nové třídy. Přesto je často možné správně řešit změny 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:

  1. Pokud se přidá nová vlastnost, která není v uloženém formátu JSON, předpokládá výchozí hodnotu.
  2. Pokud je vlastnost odebrána, která je přítomna v uloženém formátu JSON, dojde ke ztrátě předchozího obsahu.
  3. Pokud je vlastnost přejmenována, efekt je stejný, jako kdyby se odebral starý a přidal nový.
  4. Pokud se typ vlastnosti změní, aby se už nedá deserializovat z uloženého kódu JSON, vyvolá se výjimka.
  5. Pokud se typ vlastnosti změní, ale přesto může být deserializován z uloženého kódu JSON, provede se to.

Existuje mnoho možností pro přizpůsobení chování Json.NET. Chcete-li například vynutit výjimku, pokud uložený JSON obsahuje pole, které není 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.

Konstrukce entit

Někdy chceme mít větší kontrolu nad tím, jak se vytvářejí objekty 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 zvláštní inicializaci před odesláním operace do entity, která nebyla nikdy přístupná nebo která byla odstraněna. Chcete-li určit toto chování, můžete přidat podmínku DispatchAsyncpřed :

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

Vazby v 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 DispatchAsync<T> metodě. Všechny objekty předané do DispatchAsync<T> konstruktoru třídy entity se automaticky předají 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 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>();
}

Poznámka

Abyste se vyhnuli problémům se serializací, nezapomeňte vyloučit pole určená k ukládání vložených hodnot ze serializace.

Poznámka

Na rozdíl od použití injektáže konstruktoru v běžné službě .NET Azure Functions musí být deklarována metoda vstupního bodu funkcí pro entity založené na třídách static. Deklarace nestatického vstupního bodu funkce může způsobit konflikty mezi normální inicializátorem objektů Azure Functions a inicializátorem objektů 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 entit. Může to být také vhodné při implementaci knihoven, které v současné době nepodporují syntaxi založenou na třídách.

S syntaxí založenou na funkcích služba Entity Functions 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

Funkce specifické pro entity je možné získat přístup prostřednictvím objektu kontextu 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ě spouštěné entity.
  • EntityKey: klíč aktuálně spouštěné entity.
  • EntityId: ID aktuálně spouštěné entity (včetně názvu a klíče).
  • OperationName: název aktuální operace.
  • GetInput<TInput>(): získá vstup pro aktuální operaci.
  • Return(arg): vrátí hodnotu orchestraci, 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ý GetState objektem, lze ho přímo upravit kódem aplikace. Na konci není potřeba zavolat SetState znovu (ale také žádné škody). Pokud GetState<TState> se volá vícekrát, musí se použít stejný typ.

Nakonec se následující členové používají k signálu jiných entit nebo ke spuštění nových orchestrací:

  • SignalEntity(EntityId, operation, input): Odešle jednosměrnou zprávu entitě.
  • CreateNewOrchestration(orchestratorFunctionName, input): spustí novou orchestraci.

Další kroky