Delen via


Ontwikkelaarshandleiding voor duurzame entiteiten in .NET

In dit artikel beschrijven we de beschikbare interfaces voor het ontwikkelen van duurzame entiteiten met .NET in detail, waaronder voorbeelden en algemeen advies.

Entiteitsfuncties bieden serverloze toepassingsontwikkelaars een handige manier om de toepassingsstatus te organiseren als een verzameling verfijnde entiteiten. Zie het artikel Durable Entities: Concepts voor meer informatie over de onderliggende concepten.

We bieden momenteel twee API's voor het definiëren van entiteiten:

  • De syntaxis op basis van klassen vertegenwoordigt entiteiten en bewerkingen als klassen en methoden. Deze syntaxis produceert gemakkelijk leesbare code en maakt het mogelijk om bewerkingen op een door het type gecontroleerd manier aan te roepen via interfaces.

  • De syntaxis op basis van functies is een interface op lager niveau die entiteiten als functies vertegenwoordigt. Het biedt nauwkeurige controle over hoe de entiteitsbewerkingen worden verzonden en hoe de entiteitsstatus wordt beheerd.

Dit artikel richt zich voornamelijk op de syntaxis op basis van klassen, omdat we verwachten dat deze beter geschikt is voor de meeste toepassingen. De syntaxis op basis van functies kan echter geschikt zijn voor toepassingen die hun eigen abstracties willen definiëren of beheren voor entiteitsstatus en -bewerkingen. Het kan ook geschikt zijn voor het implementeren van bibliotheken waarvoor genericiteit is vereist die momenteel niet wordt ondersteund door de syntaxis op basis van klassen.

Notitie

De syntaxis op basis van klassen is slechts een laag boven op de functie gebaseerde syntaxis, dus beide varianten kunnen door elkaar worden gebruikt in dezelfde toepassing.

Entiteitsklassen definiëren

Het volgende voorbeeld is een implementatie van een Counter entiteit waarin één waarde van het type geheel getal wordt opgeslagen en vier bewerkingenAdd, Reset, en GetDelete.

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

De Run functie bevat de standaard die is vereist voor het gebruik van de op klassen gebaseerde syntaxis. Dit moet een statische Azure-functie zijn. Het wordt eenmaal uitgevoerd voor elk bewerkingsbericht dat door de entiteit wordt verwerkt. Wanneer DispatchAsync<T> de entiteit wordt aangeroepen en de entiteit nog niet in het geheugen staat, wordt er een object van het type T gemaakt en worden de velden gevuld van de laatst persistente JSON die in de opslag is gevonden (indien van toepassing). Vervolgens wordt de methode aangeroepen met de overeenkomende naam.

De EntityTrigger functie hoeft Run zich in dit voorbeeld niet in de entiteitsklasse zelf te bevinden. Deze kan zich binnen een geldige locatie voor een Azure-functie bevinden: binnen de naamruimte op het hoogste niveau of binnen een klasse op het hoogste niveau. Als de functie echter dieper is genest (bijvoorbeeld de functie wordt gedeclareerd in een geneste klasse), wordt deze functie niet herkend door de nieuwste runtime.

Notitie

De status van een op klassen gebaseerde entiteit wordt impliciet gemaakt voordat de entiteit een bewerking verwerkt en expliciet in een bewerking kan worden verwijderd door aan te roepenEntity.Current.DeleteState().

Notitie

U hebt de versie 4.0.5455 van Azure Functions Core Tools of hoger nodig om entiteiten in het geïsoleerde model uit te voeren.

Er zijn twee manieren om een entiteit te definiëren als een klasse in het geïsoleerde C#-werkrolmodel. Ze produceren entiteiten met verschillende structuren voor statusserialisatie.

Met de volgende methode wordt het hele object geserialiseerd bij het definiëren van een entiteit.

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

Een TaskEntity<TState>op -gebaseerde implementatie, waardoor het eenvoudig is om afhankelijkheidsinjectie te gebruiken. In dit geval wordt de status gedeserialiseerd op de State eigenschap en wordt er geen andere eigenschap geserialiseerd/gedeserialiseerd.

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

Waarschuwing

Bij het schrijven van entiteiten die zijn afgeleid van ITaskEntity ofTaskEntity<TState>, is het belangrijk om de triggermethode RunAsyncvoor uw entiteit niet een naam te geven. Dit veroorzaakt runtimefouten bij het aanroepen van de entiteit, omdat er een dubbelzinnige overeenkomst is met de methodenaam RunAsync omdat ITaskEntity er al een runAsync op exemplaarniveau is gedefinieerd.

Entiteiten verwijderen in het geïsoleerde model

Het verwijderen van een entiteit in het geïsoleerde model wordt bereikt door de entiteitsstatus in te stellen op null. Hoe dit wordt bereikt, is afhankelijk van welk implementatiepad voor entiteiten wordt gebruikt.

  • Wanneer u een syntaxis op basis van ITaskEntity een functie gebruikt of gebruikt, wordt het verwijderen uitgevoerd door het aanroepen TaskEntityOperation.State.SetState(null)van .
  • Bij het afleiden van TaskEntity<TState>, wordt verwijderen impliciet gedefinieerd. Het kan echter worden overschreven door een methode Delete voor de entiteit te definiëren. De status kan ook worden verwijderd uit elke bewerking via this.State = null.
    • Als u wilt verwijderen door de status in te stellen op null, moet TState u null kunnen gebruiken.
    • De impliciet gedefinieerde verwijderbewerking verwijdert niet-nullable TState.
  • Wanneer u een POCO gebruikt als uw status (niet afgeleid van TaskEntity<TState>), wordt verwijderen impliciet gedefinieerd. Het is mogelijk om de verwijderbewerking te overschrijven door een methode Delete voor de POCO te definiëren. Er is echter geen manier om de status null in te stellen op de POCO-route, zodat de impliciet gedefinieerde verwijderbewerking de enige echte verwijdering is.

Klassevereisten

Entiteitsklassen zijn POCO's (gewone oude CLR-objecten) waarvoor geen speciale superklassen, interfaces of kenmerken nodig zijn. Echter:

Bovendien moet elke methode die als bewerking moet worden aangeroepen, voldoen aan andere vereisten:

  • Een bewerking moet maximaal één argument hebben en mag geen overbelastingen of algemene typeargumenten hebben.
  • Een bewerking die moet worden aangeroepen vanuit een indeling met behulp van een interface, moet retourneren Task of Task<T>.
  • Argumenten en retourwaarden moeten serialiseerbare waarden of objecten zijn.

Wat kunnen bewerkingen doen?

Alle entiteitsbewerkingen kunnen de status van de entiteit lezen en bijwerken en wijzigingen in de status worden automatisch bewaard in de opslag. Bovendien kunnen bewerkingen externe I/O- of andere berekeningen uitvoeren, binnen de algemene limieten die gemeenschappelijk zijn voor alle Azure Functions.

Bewerkingen hebben ook toegang tot functionaliteit die wordt geboden door de Entity.Current context:

  • EntityName: de naam van de entiteit die momenteel wordt uitgevoerd.
  • EntityKey: de sleutel van de entiteit die momenteel wordt uitgevoerd.
  • EntityId: de id van de entiteit die momenteel wordt uitgevoerd (inclusief naam en sleutel).
  • SignalEntity: verzendt een eenrichtingsbericht naar een entiteit.
  • CreateNewOrchestration: start een nieuwe indeling.
  • DeleteState: verwijdert de status van deze entiteit.

We kunnen bijvoorbeeld de tellerentiteit wijzigen zodat deze een indeling start wanneer de teller 100 bereikt en de entiteits-id doorgeeft als invoerargument:

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

Rechtstreeks toegang tot entiteiten

Op klassen gebaseerde entiteiten kunnen rechtstreeks worden geopend met behulp van expliciete tekenreeksnamen voor de entiteit en de bijbehorende bewerkingen. Deze sectie bevat voorbeelden. Zie de discussie in Access-entiteiten voor een diepere uitleg van de onderliggende concepten (zoals signalen versus aanroepen).

Notitie

Waar mogelijk moet u toegang krijgen tot entiteiten via interfaces, omdat het meer typecontrole biedt.

Voorbeeld: entiteit clientsignalen

Met de volgende Azure Http-functie wordt een DELETE-bewerking geïmplementeerd met behulp van REST-conventies. Er wordt een verwijdersignaal verzonden naar de tellerentiteit waarvan de sleutel wordt doorgegeven in het URL-pad.

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

Voorbeeld: client leest de entiteitsstatus

Met de volgende Azure Http-functie wordt een GET-bewerking geïmplementeerd met behulp van REST-conventies. Hiermee wordt de huidige status van de tellerentiteit gelezen waarvan de sleutel wordt doorgegeven in het URL-pad.

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

Notitie

Het object dat wordt ReadEntityStateAsync geretourneerd door is slechts een lokale kopie, dat wil zeggen een momentopname van de entiteitsstatus vanaf een eerder tijdstip. Het kan met name verouderd zijn en het wijzigen van dit object heeft geen effect op de werkelijke entiteit.

Voorbeeld: indeling eerste signalen en vervolgens entiteit aanroepen

De volgende indeling geeft een tellerentiteit aan om deze te verhogen en roept vervolgens dezelfde entiteit aan om de meest recente waarde te lezen.

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

Voorbeeld: entiteit clientsignalen

Met de volgende Azure Http-functie wordt een DELETE-bewerking geïmplementeerd met behulp van REST-conventies. Er wordt een verwijdersignaal verzonden naar de tellerentiteit waarvan de sleutel wordt doorgegeven in het URL-pad.

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

Voorbeeld: client leest de entiteitsstatus

Met de volgende Azure Http-functie wordt een GET-bewerking geïmplementeerd met behulp van REST-conventies. Hiermee wordt de huidige status van de tellerentiteit gelezen waarvan de sleutel wordt doorgegeven in het URL-pad.

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

Voorbeeld: indeling eerste signalen en vervolgens entiteit aanroepen

De volgende indeling geeft een tellerentiteit aan om deze te verhogen en roept vervolgens dezelfde entiteit aan om de meest recente waarde te lezen.

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

Toegang tot entiteiten via interfaces

Interfaces kunnen worden gebruikt voor toegang tot entiteiten via gegenereerde proxyobjecten. Deze aanpak zorgt ervoor dat de naam en het argumenttype van een bewerking overeenkomen met wat er wordt geïmplementeerd. We raden u aan om waar mogelijk interfaces te gebruiken voor toegang tot entiteiten.

We kunnen bijvoorbeeld het voorbeeld van de teller als volgt wijzigen:

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

public class Counter : ICounter
{
    ...
}

Entiteitsklassen en entiteitsinterfaces zijn vergelijkbaar met de korrels en graaninterfaces die populair zijn bij Orleans. Zie Vergelijking met virtuele actoren voor meer informatie over overeenkomsten en verschillen tussen Durable Entities en Orleans.

Naast het bieden van typecontrole zijn interfaces handig voor een betere scheiding van zorgen binnen de toepassing. Omdat een entiteit bijvoorbeeld meerdere interfaces kan implementeren, kan één entiteit meerdere rollen leveren. Omdat een interface door meerdere entiteiten kan worden geïmplementeerd, kunnen algemene communicatiepatronen ook worden geïmplementeerd als herbruikbare bibliotheken.

Voorbeeld: entiteit clientsignalen via interface

Clientcode kan worden gebruikt SignalEntityAsync<TEntityInterface> om signalen te verzenden naar entiteiten die implementeren TEntityInterface. Bijvoorbeeld:

[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 dit voorbeeld is de proxy parameter een dynamisch gegenereerd exemplaar van ICounter, waarmee de aanroep intern wordt Delete omgezet in een signaal.

Notitie

De SignalEntityAsync API's kunnen alleen worden gebruikt voor bewerkingen in één richting. Zelfs als een bewerking retourneert Task<T>, is de waarde van de T parameter altijd null of defaultniet het werkelijke resultaat. Het is bijvoorbeeld niet zinvol om de Get bewerking aan te geven, omdat er geen waarde wordt geretourneerd. In plaats daarvan kunnen ReadStateAsync clients rechtstreeks toegang krijgen tot de tellerstatus of een orchestratorfunctie starten waarmee de Get bewerking wordt aangeroepen.

Voorbeeld: indeling eerste signalen vervolgens entiteit aanroepen via proxy

Als u een entiteit wilt aanroepen of signaleren vanuit een indeling, CreateEntityProxy kunt u samen met het interfacetype een proxy genereren voor de entiteit. Deze proxy kan vervolgens worden gebruikt om bewerkingen aan te roepen of te signaleren:

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

Impliciet worden alle bewerkingen die worden geretourneerd void gesignaleerd en alle bewerkingen die worden geretourneerd Task of Task<T> aangeroepen. U kunt dit standaardgedrag wijzigen en signaalbewerkingen zelfs als ze Taak retourneren, met behulp van de SignalEntity<IInterfaceType> methode expliciet.

Kortere optie voor het opgeven van het doel

Wanneer u een entiteit aanroept of signaleert met behulp van een interface, moet het eerste argument de doelentiteit opgeven. Het doel kan worden opgegeven door de entiteits-id op te geven, of in gevallen waarin er slechts één klasse is die de entiteit implementeert, alleen de entiteitssleutel:

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

Als alleen de entiteitssleutel is opgegeven en er tijdens runtime geen unieke implementatie kan worden gevonden, InvalidOperationException wordt deze gegenereerd.

Beperkingen voor entiteitsinterfaces

Zoals gebruikelijk moeten alle parameter- en retourtypen JSON-serializeerbaar zijn. Anders worden er serialisatie-uitzonderingen gegenereerd tijdens runtime.

We dwingen ook nog enkele regels af:

  • Entiteitsinterfaces moeten worden gedefinieerd in dezelfde assembly als de entiteitsklasse.
  • Entiteitsinterfaces mogen alleen methoden definiëren.
  • Entiteitsinterfaces mogen geen algemene parameters bevatten.
  • Methoden voor entiteitsinterface mogen niet meer dan één parameter hebben.
  • Methoden voor entiteitsinterfaces moeten retourneren void, Taskof Task<T>.

Als een van deze regels wordt geschonden, wordt er tijdens runtime een InvalidOperationException gegenereerd wanneer de interface wordt gebruikt als een typeargument voor SignalEntity, SignalEntityAsyncof CreateEntityProxy. In het uitzonderingsbericht wordt uitgelegd welke regel is verbroken.

Notitie

Interfacemethoden die worden geretourneerd void , kunnen alleen worden gesignaleerd (in één richting), niet worden aangeroepen (in twee richtingen). Interfacemethoden retourneren Task of Task<T> kunnen worden aangeroepen of gesignaleerd. Als ze worden aangeroepen, retourneren ze het resultaat van de bewerking of genereren ze uitzonderingen die door de bewerking zijn gegenereerd. Wanneer de signalering echter wordt weergegeven, retourneren ze niet het werkelijke resultaat of de uitzondering van de bewerking, maar alleen de standaardwaarde.

Dit wordt momenteel niet ondersteund in de geïsoleerde .NET-werkrol.

Entiteitsserialisatie

Omdat de status van een entiteit duurzaam blijft behouden, moet de entiteitsklasse serialiseerbaar zijn. De Durable Functions-runtime maakt gebruik van de Json.NET-bibliotheek voor dit doel, die beleid en kenmerken ondersteunt om het serialisatie- en deserialisatieproces te beheren. De meestgebruikte C#-gegevenstypen (inclusief matrices en verzamelingstypen) zijn al serialiseerbaar en kunnen eenvoudig worden gebruikt voor het definiëren van de status van duurzame entiteiten.

Json.NET kan bijvoorbeeld eenvoudig de volgende klasse serialiseren en deserialiseren:

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

    ...
}

Serialisatiekenmerken

In het bovenstaande voorbeeld hebben we ervoor gekozen om verschillende kenmerken op te nemen om de onderliggende serialisatie beter zichtbaar te maken:

  • We maken aantekeningen voor de klasse om [JsonObject(MemberSerialization.OptIn)] ons eraan te herinneren dat de klasse serialiseerbaar moet zijn en om alleen leden te behouden die expliciet zijn gemarkeerd als JSON-eigenschappen.
  • We maken aantekeningen voor de velden die moeten worden behouden [JsonProperty("name")] om ons eraan te herinneren dat een veld deel uitmaakt van de persistente entiteitsstatus en om de naam van de eigenschap op te geven die moet worden gebruikt in de JSON-weergave.

Deze kenmerken zijn echter niet vereist; andere conventies of kenmerken zijn toegestaan zolang ze met Json.NET werken. U kunt bijvoorbeeld kenmerken of helemaal geen kenmerken gebruiken [DataContract] :

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

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

Standaard wordt de naam van de klasse niet* opgeslagen als onderdeel van de JSON-weergave: dat wil gezegd, we gebruiken TypeNameHandling.None als de standaardinstelling. Dit standaardgedrag kan worden overschreven met behulp van JsonObject of JsonProperty kenmerken.

Wijzigingen aanbrengen in klassedefinities

Er is enige zorg vereist bij het aanbrengen van wijzigingen in een klassedefinitie nadat een toepassing is uitgevoerd, omdat het opgeslagen JSON-object niet meer overeenkomt met de nieuwe klassedefinitie. Toch is het vaak mogelijk om correct om te gaan met het wijzigen van gegevensindelingen, zolang men het deserialisatieproces begrijpt dat wordt gebruikt door JsonConvert.PopulateObject.

Hier volgen bijvoorbeeld enkele voorbeelden van wijzigingen en hun effect:

  • Wanneer een nieuwe eigenschap wordt toegevoegd, die niet aanwezig is in de opgeslagen JSON, wordt ervan uitgegaan dat deze standaardwaarde is.
  • Wanneer een eigenschap wordt verwijderd, die aanwezig is in de opgeslagen JSON, gaat de vorige inhoud verloren.
  • Wanneer de naam van een eigenschap wordt gewijzigd, is het effect alsof u de oude verwijdert en een nieuwe eigenschap toevoegt.
  • Wanneer het type van een eigenschap wordt gewijzigd zodat deze niet meer kan worden gedeserialiseerd vanuit de opgeslagen JSON, wordt er een uitzondering gegenereerd.
  • Wanneer het type van een eigenschap wordt gewijzigd, maar dit nog steeds kan worden gedeserialiseerd vanuit de opgeslagen JSON, doet dit dit.

Er zijn veel opties beschikbaar voor het aanpassen van het gedrag van Json.NET. Als u bijvoorbeeld een uitzondering wilt afdwingen als de opgeslagen JSON een veld bevat dat niet aanwezig is in de klasse, geeft u het kenmerk op JsonObject(MissingMemberHandling = MissingMemberHandling.Error). Het is ook mogelijk om aangepaste code te schrijven voor deserialisatie die JSON kan lezen die is opgeslagen in willekeurige indelingen.

Het standaardgedrag van serialisatie is gewijzigd inNewtonsoft.Json System.Text.Json. Zie voor meer informatie hier.

Entiteitsconstructie

Soms willen we meer controle uitoefenen over hoe entiteitsobjecten worden opgebouwd. We beschrijven nu verschillende opties voor het wijzigen van het standaardgedrag bij het maken van entiteitsobjecten.

Aangepaste initialisatie bij eerste toegang

Af en toe moeten we een speciale initialisatie uitvoeren voordat een bewerking wordt verzonden naar een entiteit die nog nooit is geopend of die is verwijderd. Als u dit gedrag wilt opgeven, kunt u een voorwaarde voor het DispatchAsyncvolgende toevoegen:

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

Bindingen in entiteitsklassen

In tegenstelling tot reguliere functies hebben entiteitsklassemethoden geen directe toegang tot invoer- en uitvoerbindingen. In plaats daarvan moeten bindingsgegevens worden vastgelegd in de declaratie van de ingangspuntfunctie en vervolgens worden doorgegeven aan de DispatchAsync<T> methode. Objecten waaraan wordt doorgegeven, worden automatisch als argument doorgegeven DispatchAsync<T> aan de constructor van de entiteitsklasse.

In het volgende voorbeeld ziet u hoe een CloudBlobContainer verwijzing van de blob-invoerbinding beschikbaar kan worden gesteld aan een entiteit op basis van een klasse.

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

Zie de documentatie over Azure Functions-triggers en bindingen voor meer informatie over bindingen in Azure Functions.

Afhankelijkheidsinjectie in entiteitsklassen

Entiteitsklassen ondersteunen afhankelijkheidsinjectie van Azure Functions. In het volgende voorbeeld ziet u hoe u een IHttpClientFactory service registreert bij een entiteit op basis van klassen.

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

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

Het volgende codefragment laat zien hoe u de geïnjecteerde service in uw entiteitsklasse kunt opnemen.

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

Aangepaste initialisatie bij eerste toegang

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

Bindingen in entiteitsklassen

In het volgende voorbeeld ziet u hoe u een blob-invoerbinding gebruikt in een op klassen gebaseerde entiteit.

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

Zie de documentatie over Azure Functions-triggers en bindingen voor meer informatie over bindingen in Azure Functions.

Afhankelijkheidsinjectie in entiteitsklassen

Entiteitsklassen ondersteunen afhankelijkheidsinjectie van Azure Functions.

Hieronder ziet u hoe u een HttpClient bestand program.cs configureert dat later in de entiteitsklasse moet worden geïmporteerd.

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

U kunt als volgt de geïnjecteerde service opnemen in uw entiteitsklasse.

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

Notitie

Om problemen met serialisatie te voorkomen, moet u ervoor zorgen dat u velden uitsluit die zijn bedoeld voor het opslaan van geïnjecteerde waarden uit de serialisatie.

Notitie

In tegenstelling tot bij het gebruik van constructorinjectie in reguliere .NET Azure Functions, moet de methode voor het toegangspunt voor functies voor entiteiten op basis van klassen worden gedeclareerdstatic. Het declareren van een niet-statisch functieinvoerpunt kan conflicten veroorzaken tussen de normale initialisatiefunctie van het Azure Functions-object en de initialisatiefunctie van het Durable Entities-object.

Syntaxis op basis van functies

Tot nu toe hebben we ons gericht op de syntaxis op basis van klassen, omdat we verwachten dat deze beter geschikt is voor de meeste toepassingen. De syntaxis op basis van functies kan echter geschikt zijn voor toepassingen die hun eigen abstracties willen definiëren of beheren voor entiteitsstatus en -bewerkingen. Het kan ook handig zijn bij het implementeren van bibliotheken waarvoor algemeenheid is vereist die momenteel niet wordt ondersteund door de syntaxis op basis van klassen.

Met de syntaxis op basis van functies verwerkt de entiteitsfunctie de verzending van de bewerking expliciet en wordt de status van de entiteit expliciet beheerd. De volgende code toont bijvoorbeeld de entiteit Teller die is geïmplementeerd met behulp van de syntaxis op basis van functies.

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

Het entiteitscontextobject

Entiteitsspecifieke functionaliteit kan worden geopend via een contextobject van het type IDurableEntityContext. Dit contextobject is beschikbaar als parameter voor de entiteitsfunctie en via de eigenschap asynchroon Entity.Current.

De volgende leden geven informatie over de huidige bewerking en stellen ons in staat om een retourwaarde op te geven.

  • EntityName: de naam van de entiteit die momenteel wordt uitgevoerd.
  • EntityKey: de sleutel van de entiteit die momenteel wordt uitgevoerd.
  • EntityId: de id van de entiteit die momenteel wordt uitgevoerd (inclusief naam en sleutel).
  • OperationName: de naam van de huidige bewerking.
  • GetInput<TInput>(): haalt de invoer voor de huidige bewerking op.
  • Return(arg): retourneert een waarde aan de indeling die de bewerking aangeroepen heeft.

De volgende leden beheren de status van de entiteit (maken, lezen, bijwerken, verwijderen).

  • HasState: of de entiteit bestaat, dat wil gezegd, heeft een bepaalde status.
  • GetState<TState>(): haalt de huidige status van de entiteit op. Als deze nog niet bestaat, wordt deze gemaakt.
  • SetState(arg): maakt of werkt de status van de entiteit bij.
  • DeleteState(): verwijdert de status van de entiteit, als deze bestaat.

Als de status die wordt geretourneerd door GetState een object is, kan deze rechtstreeks worden gewijzigd door de toepassingscode. U hoeft niet opnieuw te bellen SetState aan het einde (maar ook geen kwaad). Als GetState<TState> meerdere keren wordt aangeroepen, moet hetzelfde type worden gebruikt.

Ten slotte worden de volgende leden gebruikt om andere entiteiten aan te geven of nieuwe indelingen te starten:

  • SignalEntity(EntityId, operation, input): verzendt een eenrichtingsbericht naar een entiteit.
  • CreateNewOrchestration(orchestratorFunctionName, input): start een nieuwe indeling.
[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;
    });
}

Volgende stappen