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 Get
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>();
}
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 RunAsync
voor 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 aanroepenTaskEntityOperation.State.SetState(null)
van . - Bij het afleiden van
TaskEntity<TState>
, wordt verwijderen impliciet gedefinieerd. Het kan echter worden overschreven door een methodeDelete
voor de entiteit te definiëren. De status kan ook worden verwijderd uit elke bewerking viathis.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
.
- Als u wilt verwijderen door de status in te stellen op null, moet
- 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 methodeDelete
voor de POCO te definiëren. Er is echter geen manier om de statusnull
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:
- De klasse moet constructeerbaar zijn (zie Entiteitsconstructie).
- De klasse moet JSON-serializeerbaar zijn (zie Entiteitsserialisatie).
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
ofTask<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 default
niet 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
,Task
ofTask<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
, SignalEntityAsync
of 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 DispatchAsync
volgende 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;
});
}