Utvecklarguide till hållbara entiteter i .NET

I den här artikeln beskriver vi de tillgängliga gränssnitten för att utveckla hållbara entiteter med .NET i detalj, inklusive exempel och allmänna råd.

Entitetsfunktioner ger serverlösa programutvecklare ett bekvämt sätt att organisera programtillståndet som en samling detaljerade entiteter. Mer information om de underliggande begreppen finns i artikeln Durable Entities: Concepts (Varaktiga entiteter: Begrepp ).

Vi erbjuder för närvarande två API:er för att definiera entiteter:

  • Den klassbaserade syntaxen representerar entiteter och åtgärder som klasser och metoder. Den här syntaxen ger lättläst kod och gör att åtgärder kan anropas på ett typkontrollerat sätt via gränssnitt.

  • Den funktionsbaserade syntaxen är ett gränssnitt på lägre nivå som representerar entiteter som funktioner. Den ger exakt kontroll över hur entitetsåtgärderna skickas och hur entitetstillståndet hanteras.

Den här artikeln fokuserar främst på den klassbaserade syntaxen, eftersom vi förväntar oss att den passar bättre för de flesta program. Den funktionsbaserade syntaxen kan dock vara lämplig för program som vill definiera eller hantera sina egna abstraktioner för entitetstillstånd och åtgärder. Det kan också vara lämpligt att implementera bibliotek som kräver generiskhet som för närvarande inte stöds av den klassbaserade syntaxen.

Kommentar

Den klassbaserade syntaxen är bara ett lager ovanpå den funktionsbaserade syntaxen, så båda varianterna kan användas omväxlande i samma program.

Definiera entitetsklasser

Följande exempel är en implementering av en entitet Counter som lagrar ett enda värde av typen heltal och erbjuder fyra åtgärder Add, Reset, Getoch 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>();
}

Funktionen Run innehåller den pannplåt som krävs för att använda den klassbaserade syntaxen. Det måste vara en statisk Azure-funktion. Den körs en gång för varje åtgärdsmeddelande som bearbetas av entiteten. När DispatchAsync<T> anropas och entiteten inte redan finns i minnet konstruerar den ett objekt av typen T och fyller i fälten från den senast bevarade JSON som hittades i lagringen (om någon). Sedan anropas metoden med matchande namn.

Funktionen EntityTriggerRun i det här exemplet behöver inte finnas i själva entitetsklassen. Den kan finnas på valfri giltig plats för en Azure-funktion: i namnområdet på den översta nivån eller i en toppnivåklass. Men om den kapslas djupare (t.ex. att funktionen deklareras i en kapslad klass) identifieras inte den här funktionen av den senaste körningen.

Kommentar

Tillståndet för en klassbaserad entitet skapas implicit innan entiteten bearbetar en åtgärd och kan tas bort explicit i en åtgärd genom att anropa Entity.Current.DeleteState().

Kommentar

Du behöver Azure Functions Core Tools-versionen4.0.5455 eller senare för att köra entiteter i den isolerade modellen.

Det finns två sätt att definiera en entitet som en klass i den isolerade C#-arbetsmodellen. De skapar entiteter med olika tillståndsserialiseringsstrukturer.

Med följande metod serialiseras hela objektet när en entitet definieras.

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

En TaskEntity<TState>-baserad implementering som gör det enkelt att använda beroendeinmatning. I det här fallet deserialiseras tillståndet till State egenskapen och ingen annan egenskap serialiseras/deserialiseras.

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

Varning

När du skriver entiteter som härleds från ITaskEntity eller TaskEntity<TState>är det viktigt att inte namnge entitetsutlösarmetoden RunAsync. Detta orsakar körningsfel när du anropar entiteten eftersom det finns en tvetydig matchning med metodnamnet "RunAsync" på grund av att ITaskEntity du redan har definierat en "RunAsync" på instansnivå.

Ta bort entiteter i den isolerade modellen

Du kan ta bort en entitet i den isolerade modellen genom att ange entitetstillståndet till null. Hur detta görs beror på vilken entitetsimplementeringssökväg som används.

  • När du härleder från ITaskEntity eller använder funktionsbaserad syntax utförs borttagningen genom att anropa TaskEntityOperation.State.SetState(null).
  • När du härleder från TaskEntity<TState>definieras borttagning implicit. Det kan dock åsidosättas genom att definiera en metod Delete för entiteten. Tillstånd kan också tas bort från valfri åtgärd via this.State = null.
    • Om du vill ta bort genom att ange värdet null måste TState det vara null.
    • Den implicit definierade borttagningsåtgärden tar bort icke-nullbar TState.
  • När du använder en POCO som ditt tillstånd (som inte härleds från TaskEntity<TState>), definieras borttagning implicit. Det går att åsidosätta borttagningsåtgärden genom att definiera en metod Delete för POCO. Det finns dock inget sätt att ange tillstånd till null i POCO-vägen, så den implicit definierade borttagningsåtgärden är den enda sanna borttagningen.

Klasskrav

Entitetsklasser är POCO:er (vanliga gamla CLR-objekt) som inte kräver några särskilda superklasser, gränssnitt eller attribut. Observera följande:

Dessutom måste alla metoder som är avsedda att anropas som en åtgärd uppfylla andra krav:

  • En åtgärd måste ha högst ett argument och får inte ha några överlagringar eller allmänna typargument.
  • En åtgärd som är avsedd att anropas från en orkestrering med hjälp av ett gränssnitt måste returnera Task eller Task<T>.
  • Argument och returvärden måste vara serialiserbara värden eller objekt.

Vad kan åtgärder göra?

Alla entitetsåtgärder kan läsa och uppdatera entitetstillståndet, och ändringar i tillståndet sparas automatiskt till lagring. Dessutom kan åtgärder utföra externa I/O eller andra beräkningar inom de allmänna gränser som är gemensamma för alla Azure Functions.

Åtgärder har också åtkomst till funktioner som tillhandahålls av kontexten Entity.Current :

  • EntityName: namnet på den entitet som körs just nu.
  • EntityKey: nyckeln för den entitet som körs just nu.
  • EntityId: ID:t för den entitet som körs (innehåller namn och nyckel).
  • SignalEntity: skickar ett enkelriktad meddelande till en entitet.
  • CreateNewOrchestration: startar en ny orkestrering.
  • DeleteState: tar bort tillståndet för den här entiteten.

Vi kan till exempel ändra räknarentiteten så att den startar en orkestrering när räknaren når 100 och skickar entitets-ID:t som ett indataargument:

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

Åtkomst till entiteter direkt

Klassbaserade entiteter kan nås direkt med explicita strängnamn för entiteten och dess åtgärder. Det här avsnittet innehåller exempel. En djupare förklaring av de underliggande begreppen (till exempel signaler kontra anrop) finns i diskussionen i Åtkomstentiteter.

Kommentar

Där det är möjligt bör du komma åt entiteter via gränssnitt, eftersom det ger mer typkontroll.

Exempel: entiteten klientsignaler

Följande Azure Http-funktion implementerar en DELETE-åtgärd med hjälp av REST-konventioner. Den skickar en borttagningssignal till räknarentiteten vars nyckel skickas i URL-sökvägen.

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

Exempel: klienten läser entitetstillstånd

Följande Azure Http-funktion implementerar en GET-åtgärd med hjälp av REST-konventioner. Den läser det aktuella tillståndet för räknarentiteten vars nyckel skickas i URL-sökvägen.

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

Kommentar

Objektet som returneras av ReadEntityStateAsync är bara en lokal kopia, det vill säga en ögonblicksbild av entitetstillståndet från en tidigare tidpunkt. I synnerhet kan det vara inaktuellt, och att ändra det här objektet har ingen effekt på den faktiska entiteten.

Exempel: orkestrering först signalerar och anropar sedan entitet

Följande orkestrering signalerar en räknarentitet för att öka den och anropar sedan samma entitet för att läsa dess senaste värde.

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

Exempel: entiteten klientsignaler

Följande Azure Http-funktion implementerar en DELETE-åtgärd med hjälp av REST-konventioner. Den skickar en borttagningssignal till räknarentiteten vars nyckel skickas i URL-sökvägen.

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

Exempel: klienten läser entitetstillstånd

Följande Azure Http-funktion implementerar en GET-åtgärd med hjälp av REST-konventioner. Den läser det aktuella tillståndet för räknarentiteten vars nyckel skickas i URL-sökvägen.

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

Exempel: orkestrering först signalerar och anropar sedan entitet

Följande orkestrering signalerar en räknarentitet för att öka den och anropar sedan samma entitet för att läsa dess senaste värde.

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

Åtkomst till entiteter via gränssnitt

Gränssnitt kan användas för åtkomst till entiteter via genererade proxyobjekt. Den här metoden säkerställer att namnet och argumenttypen för en åtgärd matchar det som implementeras. Vi rekommenderar att du använder gränssnitt för åtkomst till entiteter när det är möjligt.

Vi kan till exempel ändra räknarexemplet på följande sätt:

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

public class Counter : ICounter
{
    ...
}

Entitetsklasser och entitetsgränssnitt liknar de korn- och korniga gränssnitt som populariserats av Orleans. Mer information om likheter och skillnader mellan durable entiteter och Orleans finns i Jämförelse med virtuella aktörer.

Förutom att tillhandahålla typkontroll är gränssnitt användbara för en bättre uppdelning av problem i programmet. Eftersom en entitet till exempel kan implementera flera gränssnitt kan en enda entitet hantera flera roller. Eftersom ett gränssnitt kan implementeras av flera entiteter kan allmänna kommunikationsmönster implementeras som återanvändbara bibliotek.

Exempel: klientsignaler entitet via gränssnitt

Klientkod kan använda SignalEntityAsync<TEntityInterface> för att skicka signaler till entiteter som implementerar TEntityInterface. Till exempel:

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

I det här exemplet är parametern proxy en dynamiskt genererad instans av ICounter, som internt översätter anropet till Delete till en signal.

Kommentar

SignalEntityAsync API:erna kan endast användas för enkelriktade åtgärder. Även om en åtgärd returnerar Task<T>är värdet för parametern T alltid null eller default, inte det faktiska resultatet. Det är till exempel inte meningsfullt att signalera åtgärden Get eftersom inget värde returneras. Klienter kan i stället använda antingen ReadStateAsync för att komma åt räknartillståndet direkt eller starta en orkestreringsfunktion som anropar åtgärden Get .

Exempel: orkestrering först signalerar och anropar sedan entitet via proxy

Om du vill anropa eller signalera en entitet inifrån en orkestrering CreateEntityProxy kan du, tillsammans med gränssnittstypen, generera en proxy för entiteten. Den här proxyn kan sedan användas för att anropa eller signalera åtgärder:

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

Implicit signaleras alla åtgärder som returneras void och alla åtgärder som returneras Task eller Task<T> anropas. Man kan ändra det här standardbeteendet och signalåtgärder även om de returnerar Aktivitet med hjälp SignalEntity<IInterfaceType> av metoden explicit.

Kortare alternativ för att ange målet

När du anropar eller signalerar en entitet med ett gränssnitt måste det första argumentet ange målentiteten. Målet kan anges antingen genom att ange entitets-ID eller, om det bara finns en klass som implementerar entiteten, bara entitetsnyckeln:

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

Om endast entitetsnyckeln har angetts och en unik implementering inte kan hittas vid körningen genereras InvalidOperationException .

Begränsningar för entitetsgränssnitt

Som vanligt måste alla parameter- och returtyper vara JSON-serialiserbara. Annars genereras serialiseringsundundentag vid körning.

Vi tillämpar även några fler regler:

  • Entitetsgränssnitt måste definieras i samma sammansättning som entitetsklassen.
  • Entitetsgränssnitt får endast definiera metoder.
  • Entitetsgränssnitt får inte innehålla generiska parametrar.
  • Entitetsgränssnittsmetoder får inte ha fler än en parameter.
  • Entitetsgränssnittsmetoder måste returnera void, Taskeller Task<T>.

Om någon av dessa regler överträds genereras en InvalidOperationException vid körning när gränssnittet används som ett typargument till SignalEntity, SignalEntityAsynceller CreateEntityProxy. Undantagsmeddelandet förklarar vilken regel som bröts.

Kommentar

Gränssnittsmetoder som returneras void kan bara signaleras (enkelriktade), inte anropas (tvåvägs). Gränssnittsmetoder som returnerar Task eller Task<T> kan anropas eller signaleras. Om den anropas returnerar de resultatet av åtgärden eller genererar undantag igen som genereras av åtgärden. Men när de signaleras returnerar de inte det faktiska resultatet eller undantaget från åtgärden, utan bara standardvärdet.

Detta stöds för närvarande inte i den isolerade .NET-arbetaren.

Entitets serialisering

Eftersom tillståndet för en entitet är varaktigt beständiga måste entitetsklassen vara serialiserbar. Durable Functions-körningen använder Json.NET-biblioteket för detta ändamål, som stöder principer och attribut för att styra serialiserings- och deserialiseringsprocessen. De vanligaste C#-datatyperna (inklusive matriser och samlingstyper) är redan serialiserbara och kan enkelt användas för att definiera tillståndet för varaktiga entiteter.

Till exempel kan Json.NET enkelt serialisera och deserialisera följande klass:

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

    ...
}

Serialiseringsattribut

I exemplet ovan valde vi att inkludera flera attribut för att göra den underliggande serialiseringen mer synlig:

  • Vi kommenterar klassen med [JsonObject(MemberSerialization.OptIn)] för att påminna oss om att klassen måste vara serialiserbar och att endast bevara medlemmar som uttryckligen har markerats som JSON-egenskaper.
  • Vi kommenterar de fält som ska bevaras med [JsonProperty("name")] för att påminna oss om att ett fält är en del av det bevarade entitetstillståndet och för att ange det egenskapsnamn som ska användas i JSON-representationen.

Dessa attribut krävs dock inte. andra konventioner eller attribut tillåts så länge de fungerar med Json.NET. Till exempel kan man använda [DataContract] attribut eller inga attribut alls:

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

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

Som standard lagras inte namnet på klassen* som en del av JSON-representationen: det vill säga vi använder TypeNameHandling.None som standardinställning. Det här standardbeteendet kan åsidosättas med hjälp av JsonObject eller JsonProperty attribut.

Göra ändringar i klassdefinitioner

Viss försiktighet krävs när du gör ändringar i en klassdefinition när ett program har körts, eftersom det lagrade JSON-objektet inte längre kan matcha den nya klassdefinitionen. Ändå är det ofta möjligt att hantera ändrade dataformat korrekt så länge man förstår deserialiseringsprocessen som används av JsonConvert.PopulateObject.

Här är till exempel några exempel på ändringar och deras effekt:

  • När en ny egenskap läggs till, som inte finns i den lagrade JSON:n, förutsätter den sitt standardvärde.
  • När en egenskap tas bort, som finns i den lagrade JSON-filen, går det tidigare innehållet förlorat.
  • När en egenskap byts namn blir effekten som om du tar bort den gamla och lägger till en ny.
  • När typen av en egenskap ändras så att den inte längre kan deserialiseras från den lagrade JSON:n genereras ett undantag.
  • När typen av en egenskap ändras, men den fortfarande kan deserialiseras från den lagrade JSON:en, gör den det.

Det finns många tillgängliga alternativ för att anpassa beteendet för Json.NET. Om du till exempel vill framtvinga ett undantag om den lagrade JSON-filen innehåller ett fält som inte finns i klassen anger du attributet JsonObject(MissingMemberHandling = MissingMemberHandling.Error). Det går också att skriva anpassad kod för deserialisering som kan läsa JSON som lagras i godtyckliga format.

Standardbeteendet för serialisering har ändrats från Newtonsoft.Json till System.Text.Json. Mer information finns här.

Entitetskonstruktion

Ibland vill vi utöva mer kontroll över hur entitetsobjekt skapas. Vi beskriver nu flera alternativ för att ändra standardbeteendet när du skapar entitetsobjekt.

Anpassad initiering vid första åtkomsten

Ibland behöver vi utföra en särskild initiering innan vi skickar en åtgärd till en entitet som aldrig har använts eller som har tagits bort. Om du vill ange det här beteendet kan du lägga till en villkorsstyrd före DispatchAsync:

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

Bindningar i entitetsklasser

Till skillnad från vanliga funktioner har entitetsklassmetoder inte direkt åtkomst till indata- och utdatabindningar. I stället måste bindningsdata samlas in i funktionsdeklarationen för startpunkt och skickas sedan till DispatchAsync<T> metoden. Alla objekt som skickas till DispatchAsync<T> skickas automatiskt till entitetsklasskonstruktorn som ett argument.

I följande exempel visas hur en CloudBlobContainer referens från blobindatabindningen kan göras tillgänglig för en klassbaserad entitet.

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

Mer information om bindningar i Azure Functions finns i dokumentationen om Azure Functions-utlösare och bindningar .

Beroendeinmatning i entitetsklasser

Entitetsklasser stöder Azure Functions-beroendeinmatning. I följande exempel visas hur du registrerar en IHttpClientFactory tjänst i en klassbaserad entitet.

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

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

Följande kodfragment visar hur du införlivar den inmatade tjänsten i din entitetsklass.

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

Anpassad initiering vid första åtkomsten

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

Bindningar i entitetsklasser

I följande exempel visas hur du använder en blobindatabindning i en klassbaserad entitet.

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

Mer information om bindningar i Azure Functions finns i dokumentationen om Azure Functions-utlösare och bindningar .

Beroendeinmatning i entitetsklasser

Entitetsklasser stöder Azure Functions-beroendeinmatning.

Följande visar hur du konfigurerar en HttpClient i program.cs filen som ska importeras senare i entitetsklassen.

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

Så här införlivar du den inmatade tjänsten i din entitetsklass.

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

Kommentar

Undvik problem med serialisering genom att undanta fält som är avsedda att lagra inmatade värden från serialiseringen.

Kommentar

Till skillnad från när du använder konstruktorinmatning i vanliga .NET Azure Functions måste funktionens startpunktsmetod för klassbaserade entiteter deklareras static. Om du deklarerar en icke-statisk funktionsstartpunkt kan det orsaka konflikter mellan den normala Azure Functions-objektinitieraren och objektinitieraren Durable Entities.

Funktionsbaserad syntax

Hittills har vi fokuserat på den klassbaserade syntaxen, eftersom vi förväntar oss att den passar bättre för de flesta program. Den funktionsbaserade syntaxen kan dock vara lämplig för program som vill definiera eller hantera sina egna abstraktioner för entitetstillstånd och åtgärder. Det kan också vara lämpligt när du implementerar bibliotek som kräver generiskhet som för närvarande inte stöds av den klassbaserade syntaxen.

Med den funktionsbaserade syntaxen hanterar entitetsfunktionen uttryckligen åtgärdens sändning och hanterar uttryckligen tillståndet för entiteten. Följande kod visar till exempel entiteten Counter implementerad med hjälp av den funktionsbaserade syntaxen.

[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 för entitetskontext

Entitetsspecifika funktioner kan nås via ett kontextobjekt av typen IDurableEntityContext. Det här kontextobjektet är tillgängligt som en parameter för entitetsfunktionen och via egenskapen async-local Entity.Current.

Följande medlemmar ger information om den aktuella åtgärden och tillåter oss att ange ett returvärde.

  • EntityName: namnet på den entitet som körs just nu.
  • EntityKey: nyckeln för den entitet som körs just nu.
  • EntityId: ID:t för den entitet som körs (innehåller namn och nyckel).
  • OperationName: namnet på den aktuella åtgärden.
  • GetInput<TInput>(): hämtar indata för den aktuella åtgärden.
  • Return(arg): returnerar ett värde till orkestreringen som kallade åtgärden.

Följande medlemmar hanterar tillståndet för entiteten (skapa, läsa, uppdatera, ta bort).

  • HasState: om entiteten finns, det vill säga har ett visst tillstånd.
  • GetState<TState>(): hämtar den aktuella statusen för entiteten. Om den inte redan finns skapas den.
  • SetState(arg): skapar eller uppdaterar tillståndet för entiteten.
  • DeleteState(): tar bort tillståndet för entiteten, om den finns.

Om tillståndet som returneras av GetState är ett objekt kan det ändras direkt av programkoden. Det finns ingen anledning att ringa SetState igen i slutet (men inte heller någon skada). Om GetState<TState> anropas flera gånger måste samma typ användas.

Slutligen används följande medlemmar för att signalera andra entiteter eller starta nya orkestreringar:

  • SignalEntity(EntityId, operation, input): skickar ett enkelriktad meddelande till en entitet.
  • CreateNewOrchestration(orchestratorFunctionName, input): startar en ny orkestrering.
[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;
    });
}

Nästa steg